diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a901ef0e6..d3dab20788 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,4 +32,4 @@ jobs: - name: Install dependencies run: pnpm install - name: Turbo run - run: npx turbo run build test lint + run: npx turbo run build test lint --env-mode=loose diff --git a/packages/ai/src/scripts/fine-tuning-setup.ts b/packages/ai/src/scripts/fine-tuning-setup.ts index 24726c5385..5da4d19553 100644 --- a/packages/ai/src/scripts/fine-tuning-setup.ts +++ b/packages/ai/src/scripts/fine-tuning-setup.ts @@ -1,10 +1,7 @@ import * as fs from "fs"; import { examplesToImport } from "./fine-tuning/favoriteExamples.js"; -import { - fetchCodeFromGraphQL, - fetchGroupModels, -} from "./squiggleHubHelpers.js"; +import { fetchCodeFromHub, fetchGroupModels } from "./squiggleHubHelpers.js"; interface ProcessedModel { prompt: string; @@ -98,7 +95,7 @@ async function processFavoriteExamples(): Promise { for (const example of examplesToImport) { const [owner, slug] = example.id.split("/"); try { - const code = await fetchCodeFromGraphQL(owner, slug); + const code = await fetchCodeFromHub(owner, slug); processedExamples.push({ prompt: example.prompt, response: code.trim(), diff --git a/packages/ai/src/scripts/fine-tuning/setup.ts b/packages/ai/src/scripts/fine-tuning/setup.ts index f65ece1756..1f09415907 100644 --- a/packages/ai/src/scripts/fine-tuning/setup.ts +++ b/packages/ai/src/scripts/fine-tuning/setup.ts @@ -1,9 +1,6 @@ import * as fs from "fs"; -import { - fetchCodeFromGraphQL, - fetchGroupModels, -} from "../squiggleHubHelpers.js"; +import { fetchCodeFromHub, fetchGroupModels } from "../squiggleHubHelpers.js"; import { examplesToImport } from "./favoriteExamples.js"; interface ProcessedModel { @@ -98,7 +95,7 @@ async function processFavoriteExamples(): Promise { for (const example of examplesToImport) { const [owner, slug] = example.id.split("/"); try { - const code = await fetchCodeFromGraphQL(owner, slug); + const code = await fetchCodeFromHub(owner, slug); processedExamples.push({ prompt: example.prompt, response: code.trim(), diff --git a/packages/ai/src/scripts/squiggleHubHelpers.ts b/packages/ai/src/scripts/squiggleHubHelpers.ts index b2b97d1144..454f040708 100644 --- a/packages/ai/src/scripts/squiggleHubHelpers.ts +++ b/packages/ai/src/scripts/squiggleHubHelpers.ts @@ -1,84 +1,36 @@ -import axios from "axios"; +import { z } from "zod"; -export const librariesToImport = ["ozziegooen/sTest", "ozziegooen/helpers"]; +const SERVER = "https://squigglehub.org"; -const GRAPHQL_URL = "https://squigglehub.org/api/graphql"; - -export function getQuery(owner: string, slug: string) { - return ` - query GetModelCode { - model(input: {owner: "${owner}", slug: "${slug}"}) { - ... on Model { - id - currentRevision { - content { - ... on SquiggleSnippet { - id - code - } - } - } - } - } - } - `; -} - -export async function fetchCodeFromGraphQL( +export async function fetchCodeFromHub( owner: string, slug: string ): Promise { - const query = getQuery(owner, slug); - const response = await axios.post(GRAPHQL_URL, { query }); - return response.data.data.model.currentRevision.content.code; -} + const data = await fetch( + `${SERVER}/api/get-source?${new URLSearchParams({ + owner, + slug, + })}` + ).then((res) => res.json()); + const parsed = z.object({ code: z.string() }).safeParse(data); + if (!parsed.success) { + throw new Error(`Failed to fetch source for ${owner}/${slug}`); + } -export function getGroupModelsQuery(groupSlug: string) { - return { - query: ` - query GetGroupModels($groupSlug: String!) { - group(slug: $groupSlug) { - ... on Group { - id - models(first: 50) { - edges { - node { - currentRevision { - content { - ... on SquiggleSnippet { - code - } - } - } - } - } - } - } - } - } - `, - variables: { groupSlug }, - }; + return parsed.data.code; } export async function fetchGroupModels(groupSlug: string): Promise { - try { - const query = getGroupModelsQuery(groupSlug); - const response = await axios.post(GRAPHQL_URL, query); - - if (response.status === 200) { - const models = response.data.data.group.models.edges; - console.log(`Fetched ${models.length} models from group ${groupSlug}`); - - return models.map( - (model: { node: { currentRevision: { content: { code: string } } } }) => - model.node.currentRevision.content.code - ); - } else { - throw new Error(`Error fetching group models: ${response.statusText}`); - } - } catch (error) { - console.error("Error fetching group models:", error); - throw error; + const data = await fetch( + `${SERVER}/api/get-group-models?${new URLSearchParams({ slug: groupSlug })}` + ).then((res) => res.json()); + + const parsed = z + .object({ models: z.array(z.object({ slug: z.string() })) }) + .safeParse(data); + if (!parsed.success) { + throw new Error(`Failed to fetch group models for ${groupSlug}`); } + + return parsed.data.models.map((item) => item.slug); } diff --git a/packages/components/package.json b/packages/components/package.json index 353557f47c..f1fffc6477 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -36,7 +36,7 @@ "lodash": "^4.17.21", "prettier": "^3.3.3", "react-draggable": "^4.4.6", - "react-hook-form": "^7.50.0", + "react-hook-form": "^7.53.2", "react-markdown": "^9.0.1", "reactflow": "^11.11.4", "remark-gfm": "^4.0.0", @@ -82,8 +82,8 @@ "jsdom": "^25.0.1", "postcss": "^8.4.38", "postcss-cli": "^11.0.0", - "react": "^18.2.0", - "react-dom": "^18.3.1", + "react": "19.0.0-rc-66855b96-20241106", + "react-dom": "19.0.0-rc-66855b96-20241106", "rollup-plugin-node-builtins": "^2.1.2", "storybook": "^8.1.6", "tailwindcss": "^3.4.3", diff --git a/packages/content/src/collections/squiggleAiLibraries.ts b/packages/content/src/collections/squiggleAiLibraries.ts index 4b64fe7ec8..f395b9d669 100644 --- a/packages/content/src/collections/squiggleAiLibraries.ts +++ b/packages/content/src/collections/squiggleAiLibraries.ts @@ -1,33 +1,34 @@ import { defineCollection } from "@content-collections/core"; +import { z } from "zod"; -const GRAPHQL_URL = "https://squigglehub.org/api/graphql"; +const SERVER = "https://squigglehub.org"; -export function getQuery(owner: string, slug: string) { +function getGraphqlQuery(owner: string, slug: string) { return ` - query GetModelCode { - model(input: {owner: "${owner}", slug: "${slug}"}) { - ... on Model { - id - currentRevision { - content { - ... on SquiggleSnippet { - id - code + query GetModelCode { + model(input: {owner: "${owner}", slug: "${slug}"}) { + ... on Model { + id + currentRevision { + content { + ... on SquiggleSnippet { + id + code + } } } } } } - } - `; + `; } -export async function fetchCodeFromGraphQL( +export async function fetchCodeFromHubLegacy( owner: string, slug: string ): Promise { - const query = getQuery(owner, slug); - const response = await fetch(GRAPHQL_URL, { + const query = getGraphqlQuery(owner, slug); + const response = await fetch("https://squigglehub.org/api/graphql", { headers: { "Content-Type": "application/json", }, @@ -42,6 +43,29 @@ export async function fetchCodeFromGraphQL( return code; } +// copy-pasted from squiggle/packages/ai/src/scripts/squiggleHubHelpers.ts +export async function fetchCodeFromHub( + owner: string, + slug: string +): Promise { + try { + const data = await fetch( + `${SERVER}/api/get-source?${new URLSearchParams({ + owner, + slug, + })}` + ).then((res) => res.json()); + const parsed = z.object({ code: z.string() }).safeParse(data); + if (!parsed.success) { + throw new Error(`Failed to fetch source for ${owner}/${slug}`); + } + + return parsed.data.code; + } catch (e) { + return await fetchCodeFromHubLegacy(owner, slug); + } +} + export const squiggleAiLibraries = defineCollection({ name: "squiggleAiLibraries", directory: "content/squiggleAiLibraries", @@ -52,7 +76,7 @@ export const squiggleAiLibraries = defineCollection({ slug: z.string(), }), transform: async (data) => { - const code = await fetchCodeFromGraphQL(data.owner, data.slug); + const code = await fetchCodeFromHub(data.owner, data.slug); const importName = `hub:${data.owner}/${data.slug}`; return { ...data, importName, code }; diff --git a/packages/hub/.gitignore b/packages/hub/.gitignore index c04a9a2c17..2bfa825fa2 100644 --- a/packages/hub/.gitignore +++ b/packages/hub/.gitignore @@ -4,7 +4,3 @@ /tsconfig.tsbuildinfo /.next /.vscode - -# Ignore all of the generated files, but keep the directory -src/__generated__/* -!src/__generated__/.gitkeep diff --git a/packages/hub/.prettierignore b/packages/hub/.prettierignore index a4821cf008..f72a0b101a 100644 --- a/packages/hub/.prettierignore +++ b/packages/hub/.prettierignore @@ -1,5 +1,2 @@ .vscode .next -src/__generated__ -schema.graphql -test/gql-gen diff --git a/packages/hub/README.md b/packages/hub/README.md index 6464989d03..44284e2c08 100644 --- a/packages/hub/README.md +++ b/packages/hub/README.md @@ -38,16 +38,7 @@ The basic loop is: ## Notes on changing the schema -`pnpm run gen` is a pipeline with three steps: - -1. First, `gen:prisma` generates `@prisma/client` from `prisma/schema.prisma`. -2. Then `gen:schema` generates `schema.graphql` from our GraphQL server code in `src/graphql/`. -3. Finally, `gen:relay` runs [relay-compiler](https://relay.dev/docs/guides/compiler/), which generates `src/__generated__` files based on `schema.graphql`. - -So: - -- for the database schema, `prisma/schema.prisma` is the source of truth -- for the GraphQL schema, the TypeScript code in `src/graphql/` is the source of truth, while `schema.graphql` is auto-generated (but it still should be committed to the repo) +`pnpm run gen` generates `@prisma/client` from `prisma/schema.prisma`. If it looks like VS Code doesn't see your latest changes, try this: @@ -56,18 +47,8 @@ If it looks like VS Code doesn't see your latest changes, try this: Note: the "Restart TS Server" step is necessary because `@prisma/client` code is out of the main source tree, and VS Code won't notice that it has updated. But restarting TS Server is slow, so a better solution is to keep `@prisma/client` source code open (open `src/prisma.ts`, then "Go to definition" on `PrismaClient`). Then VS Code will watch it for changes. -For Relay-generated files under `src/__generated__`, VS Code usually detects the changes automatically. - -Another note is that with the correct setup, out of `pnpm gen:prisma`, `pnpm gen:schema` and `pnpm gen:relay` pipeline steps, only `gen:schema` is necessary: - -- `@prisma/client` will be regenerated on `prisma db push` or `prisma migrate dev` -- `gen:schema`, which calls the `src/graphql/print-schema.ts` script, doesn't have the watch mode, so it _is_ necessary to call it after you edit any `src/graphql/` code -- `gen:relay` (`relay-compiler`) will run in watch mode if you use [Relay GraphQL extension](https://marketplace.visualstudio.com/items?itemName=meta.relay) and enable `relay.autoStartCompiler` option in VS Code settings - ## Other notes -[How to load GraphQL data in Next.js pages](/docs/relay-pages.md) - [Common workflow for updating Prisma schema](https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema) # Deployment @@ -79,13 +60,3 @@ Squiggle Hub is deployed on [Vercel](https://vercel.com/) automatically when the The production database is migrated by [this GitHub Action](https://github.com/quantified-uncertainty/squiggle/blob/main/.github/workflows/prisma-migrate-prod.yml). **Important: it should be invoked _before_ merging any PR that changes the schema.** - -## Debugging - -If you get an error like: - -``` -PothosSchemaError [GraphQLError]: Ref ObjectRef has not been implemented -``` - -Make sure that any new files in `src/graphql/types` have been added to `src/schema.ts`, or something that references that. diff --git a/packages/hub/codegen.ts b/packages/hub/codegen.ts deleted file mode 100644 index 9b22fafa40..0000000000 --- a/packages/hub/codegen.ts +++ /dev/null @@ -1,14 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { type CodegenConfig } from "@graphql-codegen/cli"; - -const config: CodegenConfig = { - schema: "./schema.graphql", - documents: ["test/graphql/**/*.ts"], - generates: { - "./test/gql-gen/": { - preset: "client-preset", - }, - }, -}; - -export default config; diff --git a/packages/hub/docs/nextjs.md b/packages/hub/docs/nextjs.md new file mode 100644 index 0000000000..d0633c7e71 --- /dev/null +++ b/packages/hub/docs/nextjs.md @@ -0,0 +1,31 @@ +# Notes on Next.js + +## Conventions + +### Component files + +- store single use components in `src/app`, next to their `page.tsx` and `layout.tsx` +- if the component is shared between multiple pages, store it in `src/{topic}/components/`, where `{topic}` is something like "models" or "relative-values" +- if the component doesn't have an obvious topic, e.g. if it's a generic UI component, store it in `src/components/` + +### Actions + +- store actions in `src/{topic}/actions/`, where `{topic}` is something like "models" or "relative-values" +- name actions like this: `doSomethingAction` +- use `next-safe-action` to define all actions +- return _something_ from actions, even if it's just `"ok"`; some wrappers check whether `data` on the action is defined + +### Data loading + +- all data loading functions that expose data to the frontend should go in `src/{topic}/data/` +- data loading functions should sanitize the data that they select from the database, to avoid security issues + +## Loading pages + +Avoid generic `loading.tsx` files. Thoughtful loading states are good, but the generic top-level loading state was harmful: + +- it doesn't match the final rendered state so it looks more like a flash of unrelated content than a skeleton +- loading state means that the previous page will disappear faster than necessary, and that's bad; in other words, the loading state is only useful when it hints where the content will appear +- in addition, the loading state means that `` prefetching won't work + +Avoid nested `loading.tsx` files. I'm not sure why but they might cause double flash of loading states, similar to this thread: https://www.reddit.com/r/nextjs/comments/17hn1a5/nested_loading_states/ diff --git a/packages/hub/docs/relay-pages.md b/packages/hub/docs/relay-pages.md deleted file mode 100644 index 6b1f7dfeac..0000000000 --- a/packages/hub/docs/relay-pages.md +++ /dev/null @@ -1,72 +0,0 @@ -# How to load GraphQL data in Next.js pages - -Based on https://github.com/relayjs/relay-examples/tree/main/issue-tracker-next-v13. - -For any new page that needs GraphQL data you'll need two files: - -1. `page.tsx` (usual Next.js page file) -2. `MyPage.tsx` (gobally unique semantic name, like `ModelPage` or `DefinitionPage`) - -## page.tsx - -**Don't** put `"use client"` in `page.tsx`; it should an RSC. - -Call `loadSerializableQuery` to get a query: - -```typescript -// Generated based on a query definition from `MyPage.tsx` by `relay-compiler`. -// You don't have to rename `MyPageQuery` type, but this pattern will be copy-pasted in all pages, -// so it's nice to have the identical type and var names in the following code. -// (There's almost always only a single query per page, so there's no risk of name collisions.) -import QueryNode, { MyPageQuery as QueryType } from from "@/__generated__/MyPageQuery.graphql"; - -export default async function Page() { - const query = await loadSerializableQuery(QueryNode.params, { - ... // query variables - }); - - return ; -} -``` - -## MyPage.tsx - -`MyPage` component must have `"use client"` declaration on top. - -It should contain: - -1. GraphQL query, like this: - -```typescript -// In Relay, query name ("MyPageQuery") must match the file name ("MyPage"). -// So we store this definition here and not in `page.tsx`. -const Query = graphql` - query MyPageQuery { - myField { - ... - } - } -`; -``` - -2. A React component that takes a serializable query as a parameter: - -```typescript -// Same import as in `page.tsx`. -import QueryNode, { MyPageQuery as QueryType } from from "@/__generated__/MyPageQuery.graphql"; - -export const MyPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const queryRef = useSerializablePreloadedQuery(query); - const { myField: result } = usePreloadedQuery(Query, queryRef); - - ... -} -``` - -# Notes - -- If you use `useLazyLoadQuery` instead of this pattern, the performance will be worse -- It's ok to use `useLazyLoadQuery` in the components that render on demand (e.g. data that shows when you open a dropdown) -- If you need to load GraphQL data in the Next.js layout, use `layout.tsx` and `MyLayout.tsx`; everything else should be the same diff --git a/packages/hub/esbuild.cjs b/packages/hub/esbuild.cjs index 457ee46e08..f9cf0be4cc 100644 --- a/packages/hub/esbuild.cjs +++ b/packages/hub/esbuild.cjs @@ -39,6 +39,6 @@ await (async () => { `, }, outfile: `./dist/scripts/${name}.mjs`, - external: ["server-only"], + external: ["server-only", "@opentelemetry/api"], }); } diff --git a/packages/hub/graphql.config.yaml b/packages/hub/graphql.config.yaml deleted file mode 100644 index 0754fbf918..0000000000 --- a/packages/hub/graphql.config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -schema: ./schema.graphql -documents: - - src/**/*.tsx - - src/**/*.ts - - test/**/*.ts diff --git a/packages/hub/next.config.mjs b/packages/hub/next.config.mjs index 9d0ed58f98..a3ec7ca6e3 100644 --- a/packages/hub/next.config.mjs +++ b/packages/hub/next.config.mjs @@ -4,15 +4,6 @@ const nextConfig = { experimental: { runtime: "nodejs", }, - compiler: { - relay: { - // This should match relay.config.js - src: "./src", - language: "typescript", - artifactDirectory: "./src/__generated__", - eagerEsModules: false, - }, - }, redirects: async () => [ { source: "/users/:username/models/:slug*", diff --git a/packages/hub/package.json b/packages/hub/package.json index b8a03074fd..3159874aa4 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -12,9 +12,7 @@ "dev": "next dev -p 3001 --turbo", "start": "__NEXT_PRIVATE_PREBUNDLED_REACT=next next start", "gen:prisma": "PRISMA_HIDE_UPDATE_MESSAGE=1 prisma generate --no-hints", - "gen:relay": "relay-compiler", - "gen:schema": "pnpm build:esbuild && node ./dist/scripts/print-schema.mjs", - "gen": "pnpm gen:prisma && pnpm gen:schema && pnpm gen:relay", + "gen": "pnpm gen:prisma", "gen:watch": "nodemon --watch src --ext ts,tsx,prisma --exec 'pnpm run gen'", "build:esbuild": "node ./esbuild.cjs", "build:ts": "pnpm gen && tsc", @@ -23,18 +21,10 @@ "lint": "prettier --check . && next lint", "format": "prettier --write .", "test:manual": "dotenv -e .env.test -- jest -i", - "build-last-revision": "pnpm build:esbuild && node ./dist/scripts/buildRecentModelRevision/main.mjs" + "build-last-revision": "pnpm build:esbuild && NODE_OPTIONS=--conditions=react-server node ./dist/scripts/buildRecentModelRevision/main.mjs" }, "dependencies": { "@auth/prisma-adapter": "^2.7.4", - "@pothos/core": "^3.41.1", - "@pothos/plugin-errors": "^3.11.1", - "@pothos/plugin-prisma": "^3.65.2", - "@pothos/plugin-relay": "^3.46.0", - "@pothos/plugin-scope-auth": "^3.22.0", - "@pothos/plugin-simple-objects": "^3.7.0", - "@pothos/plugin-validation": "^3.10.1", - "@pothos/plugin-with-input": "^3.10.1", "@prisma/client": "5.22.0", "@quri/squiggle-ai": "workspace:*", "@quri/squiggle-components": "workspace:*", @@ -46,32 +36,27 @@ "clsx": "^2.1.1", "d3": "^7.9.0", "date-fns": "^3.6.0", - "graphql": "^16.8.1", - "graphql-yoga": "^5.1.1", "immutable": "^4.3.6", "invariant": "^2.2.4", "lodash": "^4.17.21", "next": "^15.0.3", "next-auth": "5.0.0-beta.25", + "next-safe-action": "^7.9.9", "nodemailer": "^6.9.13", "pako": "^2.1.0", - "react": "^18.2.0", - "react-dom": "^18.3.1", - "react-hook-form": "^7.50.0", + "react": "19.0.0-rc-66855b96-20241106", + "react-dom": "19.0.0-rc-66855b96-20241106", + "react-hook-form": "^7.53.2", "react-icons": "^5.2.1", "react-loading-skeleton": "^3.4.0", "react-markdown": "^9.0.1", - "react-relay": "^16.2.0", "react-select": "^5.8.0", - "relay-runtime": "^16.2.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", + "server-only": "^0.0.1", "zod": "^3.23.8" }, "devDependencies": { - "@graphql-codegen/cli": "^5.0.2", - "@graphql-codegen/client-preset": "^4.2.5", - "@graphql-typed-document-node/core": "^3.2.0", "@parcel/watcher": "^2.4.1", "@quri/configs": "workspace:*", "@types/d3": "^7.4.3", @@ -81,21 +66,16 @@ "@types/node": "^20.12.7", "@types/pako": "^2.0.3", "@types/react": "^18.3.3", - "@types/react-relay": "^16.0.6", - "@types/relay-runtime": "^14.1.23", - "babel-plugin-relay": "^16.2.0", "dotenv-cli": "^7.4.2", "esbuild": "^0.20.2", "eslint": "^8.57.0", "eslint-config-next": "^14.2.3", - "graphql": "^16.8.1", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "nodemon": "^3.1.0", "postcss": "^8.4.38", "prettier": "^3.3.3", "prisma": "^5.22.0", - "relay-compiler": "^16.2.0", "tailwindcss": "^3.4.3", "tsx": "^4.19.1", "typescript": "^5.6.3" diff --git a/packages/hub/prisma/schema.prisma b/packages/hub/prisma/schema.prisma index b45c0ee196..0b8e2617f0 100644 --- a/packages/hub/prisma/schema.prisma +++ b/packages/hub/prisma/schema.prisma @@ -6,10 +6,6 @@ generator client { previewFeatures = ["postgresqlExtensions", "fullTextSearch"] } -generator pothos { - provider = "prisma-pothos-types" -} - datasource db { provider = "postgresql" url = env("DATABASE_URL") diff --git a/packages/hub/relay.config.js b/packages/hub/relay.config.js deleted file mode 100644 index b41c6c86c3..0000000000 --- a/packages/hub/relay.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - src: "./src", - language: "typescript", - artifactDirectory: "./src/__generated__", - schema: "./schema.graphql", - // https://github.com/facebook/relay/releases/tag/v16.0.0 - typescriptExcludeUndefinedFromNullableUnion: true, - exclude: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"], -}; diff --git a/packages/hub/schema.graphql b/packages/hub/schema.graphql deleted file mode 100644 index add529deeb..0000000000 --- a/packages/hub/schema.graphql +++ /dev/null @@ -1,824 +0,0 @@ -type AcceptReusableGroupInviteTokenResult { - membership: UserGroupMembership! -} - -type AddUserToGroupResult { - membership: UserGroupMembership! -} - -type AdminRebuildSearchIndexResult { - ok: Boolean! -} - -type AdminUpdateModelVersionResult { - model: Model! -} - -type BaseError implements Error { - message: String! -} - -type BuildRelativeValuesCacheResult { - relativeValuesExport: RelativeValuesExport! -} - -type CancelGroupInviteResult { - invite: GroupInvite! -} - -type ClearRelativeValuesCacheResult { - relativeValuesExport: RelativeValuesExport! -} - -type CreateGroupResult { - group: Group! -} - -type CreateRelativeValuesDefinitionResult { - definition: RelativeValuesDefinition! -} - -type CreateReusableGroupInviteTokenResult { - group: Group! -} - -type CreateSquiggleSnippetModelResult { - model: Model! -} - -input DefinitionRefInput { - owner: String! - slug: String! -} - -type DeleteMembershipResult { - ok: Boolean! -} - -type DeleteModelResult { - ok: Boolean! -} - -type DeleteRelativeValuesDefinitionResult { - ok: Boolean! -} - -type DeleteReusableGroupInviteTokenResult { - group: Group! -} - -type EmailGroupInvite implements GroupInvite & Node { - email: String! - group: Group! - id: ID! - role: MembershipRole! -} - -interface Error { - message: String! -} - -type GlobalStatistics { - models: Int! - relativeValuesDefinitions: Int! - users: Int! -} - -type Group implements Node & Owner { - createdAtTimestamp: Float! - id: ID! - inviteForMe: GroupInvite - invites(after: String, before: String, first: Int, last: Int): GroupInviteConnection - memberships(after: String, before: String, first: Int, last: Int): UserGroupMembershipConnection! - models(after: String, before: String, first: Int, last: Int): ModelConnection! - myMembership: UserGroupMembership - reusableInviteToken: String - slug: String! - updatedAtTimestamp: Float! - variableRevisions(after: String, before: String, first: Int, last: Int): VariableRevision! -} - -type GroupConnection { - edges: [GroupEdge!]! - pageInfo: PageInfo! -} - -type GroupEdge { - cursor: String! - node: Group! -} - -interface GroupInvite { - group: Group! - id: ID! - role: MembershipRole! -} - -type GroupInviteConnection { - edges: [GroupInviteEdge!]! - pageInfo: PageInfo! -} - -type GroupInviteEdge { - cursor: String! - node: GroupInvite! -} - -enum GroupInviteReaction { - Accept - Decline -} - -input GroupsQueryInput { - """List only groups that you're a member of""" - myOnly: Boolean - slugContains: String -} - -type InviteUserToGroupResult { - invite: GroupInvite! -} - -type Me { - asUser: User! - email: String! - username: String -} - -enum MembershipRole { - Admin - Member -} - -type Model implements Node { - createdAtTimestamp: Float! - currentRevision: ModelRevision! - id: ID! - isEditable: Boolean! - isPrivate: Boolean! - lastRevisionWithBuild: ModelRevision - owner: Owner! - revision(id: ID!): ModelRevision! - revisions(after: String, before: String, first: Int, last: Int): ModelRevisionConnection! - slug: String! - updatedAtTimestamp: Float! - variables: [Variable!]! -} - -type ModelConnection { - edges: [ModelEdge!]! - pageInfo: PageInfo! -} - -union ModelContent = SquiggleSnippet - -type ModelEdge { - cursor: String! - node: Model! -} - -type ModelRevision implements Node { - author: User - buildStatus: ModelRevisionBuildStatus! - comment: String! - content: ModelContent! - createdAtTimestamp: Float! - exportNames: [String!]! - forRelativeValues(input: ModelRevisionForRelativeValuesInput!): ModelRevisionForRelativeValuesResult! - id: ID! - lastBuild: ModelRevisionBuild - model: Model! - relativeValuesExports: [RelativeValuesExport!]! - variableRevisions: [VariableRevision!]! -} - -type ModelRevisionBuild implements Node { - createdAtTimestamp: Float! - errors: [String!]! - id: ID! - modelRevision: ModelRevision! - runSeconds: Float! -} - -enum ModelRevisionBuildStatus { - Failure - Pending - Skipped - Success -} - -type ModelRevisionConnection { - edges: [ModelRevisionEdge!]! - pageInfo: PageInfo! -} - -type ModelRevisionEdge { - cursor: String! - node: ModelRevision! -} - -input ModelRevisionForRelativeValuesInput { - for: ModelRevisionForRelativeValuesSlugOwnerInput - variableName: String! -} - -union ModelRevisionForRelativeValuesResult = BaseError | NotFoundError | RelativeValuesExport - -input ModelRevisionForRelativeValuesSlugOwnerInput { - owner: String! - slug: String! -} - -type ModelsByVersion { - count: Int! - models: [Model!]! - privateCount: Int! - version: String! -} - -type MoveModelResult { - model: Model! -} - -type Mutation { - acceptReusableGroupInviteToken(input: MutationAcceptReusableGroupInviteTokenInput!): MutationAcceptReusableGroupInviteTokenResult! - addUserToGroup(input: MutationAddUserToGroupInput!): MutationAddUserToGroupResult! - - """Admin-only query for rebuilding the search index""" - adminRebuildSearchIndex: MutationAdminRebuildSearchIndexResult! - - """Admin-only query for upgrading model versions""" - adminUpdateModelVersion(input: MutationAdminUpdateModelVersionInput!): MutationAdminUpdateModelVersionResult! - buildRelativeValuesCache(input: MutationBuildRelativeValuesCacheInput!): MutationBuildRelativeValuesCacheResult! - cancelGroupInvite(input: MutationCancelGroupInviteInput!): MutationCancelGroupInviteResult! - clearRelativeValuesCache(input: MutationClearRelativeValuesCacheInput!): MutationClearRelativeValuesCacheResult! - createGroup(input: MutationCreateGroupInput!): MutationCreateGroupResult! - createRelativeValuesDefinition(input: MutationCreateRelativeValuesDefinitionInput!): MutationCreateRelativeValuesDefinitionResult! - - """ - Create or replace a reusable invite token for a group, available as `reusableInviteToken` field on group object. - - You must be an admin of the group to call this mutation. Previous invite token, if it existed, will stop working. - """ - createReusableGroupInviteToken(input: MutationCreateReusableGroupInviteTokenInput!): MutationCreateReusableGroupInviteTokenResult! - createSquiggleSnippetModel(input: MutationCreateSquiggleSnippetModelInput!): MutationCreateSquiggleSnippetModelResult! - deleteMembership(input: MutationDeleteMembershipInput!): MutationDeleteMembershipResult! - deleteModel(input: MutationDeleteModelInput!): MutationDeleteModelResult! - deleteRelativeValuesDefinition(input: MutationDeleteRelativeValuesDefinitionInput!): MutationDeleteRelativeValuesDefinitionResult! - - """Disable a reusable invite token for a group.""" - deleteReusableGroupInviteToken(input: MutationDeleteReusableGroupInviteTokenInput!): MutationDeleteReusableGroupInviteTokenResult! - inviteUserToGroup(input: MutationInviteUserToGroupInput!): MutationInviteUserToGroupResult! - moveModel(input: MutationMoveModelInput!): MutationMoveModelResult! - reactToGroupInvite(input: MutationReactToGroupInviteInput!): MutationReactToGroupInviteResult! - setUsername(username: String!): MutationSetUsernameResult! - updateGroupInviteRole(input: MutationUpdateGroupInviteRoleInput!): MutationUpdateGroupInviteRoleResult! - updateMembershipRole(input: MutationUpdateMembershipRoleInput!): MutationUpdateMembershipRoleResult! - updateModelPrivacy(input: MutationUpdateModelPrivacyInput!): MutationUpdateModelPrivacyResult! - updateModelSlug(input: MutationUpdateModelSlugInput!): MutationUpdateModelSlugResult! - updateRelativeValuesDefinition(input: MutationUpdateRelativeValuesDefinitionInput!): MutationUpdateRelativeValuesDefinitionResult! - updateSquiggleSnippetModel(input: MutationUpdateSquiggleSnippetModelInput!): MutationUpdateSquiggleSnippetModelResult! - validateReusableGroupInviteToken(input: MutationValidateReusableGroupInviteTokenInput!): MutationValidateReusableGroupInviteTokenResult! -} - -input MutationAcceptReusableGroupInviteTokenInput { - groupSlug: String! - inviteToken: String! -} - -union MutationAcceptReusableGroupInviteTokenResult = AcceptReusableGroupInviteTokenResult | BaseError - -input MutationAddUserToGroupInput { - group: String! - role: MembershipRole! - username: String! -} - -union MutationAddUserToGroupResult = AddUserToGroupResult | BaseError | ValidationError - -union MutationAdminRebuildSearchIndexResult = AdminRebuildSearchIndexResult | BaseError - -input MutationAdminUpdateModelVersionInput { - modelId: String! - version: String! -} - -union MutationAdminUpdateModelVersionResult = AdminUpdateModelVersionResult | BaseError - -input MutationBuildRelativeValuesCacheInput { - exportId: String! -} - -union MutationBuildRelativeValuesCacheResult = BaseError | BuildRelativeValuesCacheResult - -input MutationCancelGroupInviteInput { - inviteId: String! -} - -union MutationCancelGroupInviteResult = BaseError | CancelGroupInviteResult - -input MutationClearRelativeValuesCacheInput { - exportId: String! -} - -union MutationClearRelativeValuesCacheResult = BaseError | ClearRelativeValuesCacheResult - -input MutationCreateGroupInput { - slug: String! -} - -union MutationCreateGroupResult = BaseError | CreateGroupResult - -input MutationCreateRelativeValuesDefinitionInput { - clusters: [RelativeValuesClusterInput!]! - - """ - Optional, if not set, definition will be created on current user's account - """ - groupSlug: String - items: [RelativeValuesItemInput!]! - recommendedUnit: String - slug: String! - title: String! -} - -union MutationCreateRelativeValuesDefinitionResult = BaseError | CreateRelativeValuesDefinitionResult | ValidationError - -input MutationCreateReusableGroupInviteTokenInput { - slug: String! -} - -union MutationCreateReusableGroupInviteTokenResult = BaseError | CreateReusableGroupInviteTokenResult - -input MutationCreateSquiggleSnippetModelInput { - """Squiggle source code""" - code: String! - - """Optional, if not set, model will be created on current user's account""" - groupSlug: String - - """Defaults to false""" - isPrivate: Boolean - - """A unique seed, used for calculation""" - seed: String! - slug: String! - version: String! -} - -union MutationCreateSquiggleSnippetModelResult = BaseError | CreateSquiggleSnippetModelResult | ValidationError - -input MutationDeleteMembershipInput { - group: String! - user: String! -} - -union MutationDeleteMembershipResult = BaseError | DeleteMembershipResult - -input MutationDeleteModelInput { - owner: String! - slug: String! -} - -union MutationDeleteModelResult = BaseError | DeleteModelResult | NotFoundError | ValidationError - -input MutationDeleteRelativeValuesDefinitionInput { - owner: String! - slug: String! -} - -union MutationDeleteRelativeValuesDefinitionResult = BaseError | DeleteRelativeValuesDefinitionResult - -input MutationDeleteReusableGroupInviteTokenInput { - slug: String! -} - -union MutationDeleteReusableGroupInviteTokenResult = BaseError | DeleteReusableGroupInviteTokenResult - -input MutationInviteUserToGroupInput { - group: String! - role: MembershipRole! - username: String! -} - -union MutationInviteUserToGroupResult = BaseError | InviteUserToGroupResult | ValidationError - -input MutationMoveModelInput { - newOwner: String! - oldOwner: String! - slug: String! -} - -union MutationMoveModelResult = BaseError | MoveModelResult | NotFoundError | ValidationError - -input MutationReactToGroupInviteInput { - action: GroupInviteReaction! - inviteId: String! -} - -union MutationReactToGroupInviteResult = BaseError | ReactToGroupInviteResult - -union MutationSetUsernameResult = BaseError | Me | ValidationError - -input MutationUpdateGroupInviteRoleInput { - inviteId: String! - role: MembershipRole! -} - -union MutationUpdateGroupInviteRoleResult = BaseError | UpdateGroupInviteRoleResult - -input MutationUpdateMembershipRoleInput { - group: String! - role: MembershipRole! - user: String! -} - -union MutationUpdateMembershipRoleResult = BaseError | UpdateMembershipRoleResult - -input MutationUpdateModelPrivacyInput { - isPrivate: Boolean! - owner: String! - slug: String! -} - -union MutationUpdateModelPrivacyResult = BaseError | UpdateModelPrivacyResult - -input MutationUpdateModelSlugInput { - newSlug: String! - oldSlug: String! - owner: String! -} - -union MutationUpdateModelSlugResult = BaseError | UpdateModelSlugResult - -input MutationUpdateRelativeValuesDefinitionInput { - clusters: [RelativeValuesClusterInput!]! - items: [RelativeValuesItemInput!]! - owner: String! - recommendedUnit: String - slug: String! - title: String! -} - -union MutationUpdateRelativeValuesDefinitionResult = BaseError | UpdateRelativeValuesDefinitionResult - -input MutationUpdateSquiggleSnippetModelInput { - comment: String - content: SquiggleSnippetContentInput! - owner: String! - relativeValuesExports: [RelativeValuesExportInput!] - slug: String! -} - -union MutationUpdateSquiggleSnippetModelResult = BaseError | UpdateSquiggleSnippetResult - -input MutationValidateReusableGroupInviteTokenInput { - groupSlug: String! - inviteToken: String! -} - -union MutationValidateReusableGroupInviteTokenResult = BaseError | ValidateReusableGroupInviteTokenResult - -interface Node { - id: ID! -} - -type NotFoundError implements Error { - message: String! -} - -interface Owner { - id: ID! - slug: String! -} - -type PageInfo { - endCursor: String - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String -} - -type Query { - globalStatistics: GlobalStatistics! - group(slug: String!): QueryGroupResult! - groups(after: String, before: String, first: Int, input: GroupsQueryInput, last: Int): GroupConnection! - me: Me! - model(input: QueryModelInput!): QueryModelResult! - models(after: String, before: String, first: Int, last: Int): ModelConnection! - - """Admin-only query for listing models in /admin UI""" - modelsByVersion: [ModelsByVersion!]! - node(id: ID!): Node - nodes(ids: [ID!]!): [Node]! - relativeValuesDefinition(input: QueryRelativeValuesDefinitionInput!): QueryRelativeValuesDefinitionResult! - relativeValuesDefinitions(after: String, before: String, first: Int, input: RelativeValuesDefinitionsQueryInput, last: Int): RelativeValuesDefinitionConnection! - runSquiggle(code: String!, seed: String): SquiggleOutput! - search(after: String, before: String, first: Int, last: Int, text: String!): QuerySearchResult! - userByUsername(username: String!): QueryUserByUsernameResult! - users(after: String, before: String, first: Int, input: UsersQueryInput, last: Int): QueryUsersConnection! - variable(input: QueryVariableInput!): QueryVariableResult! - variables(after: String, before: String, first: Int, input: VariableQueryInput, last: Int): VariableConnection! -} - -union QueryGroupResult = BaseError | Group | NotFoundError - -input QueryModelInput { - owner: String! - slug: String! -} - -union QueryModelResult = BaseError | Model | NotFoundError - -input QueryRelativeValuesDefinitionInput { - owner: String! - slug: String! -} - -union QueryRelativeValuesDefinitionResult = BaseError | NotFoundError | RelativeValuesDefinition - -type QuerySearchConnection { - edges: [SearchEdge!]! - pageInfo: PageInfo! -} - -union QuerySearchResult = BaseError | QuerySearchConnection - -union QueryUserByUsernameResult = BaseError | NotFoundError | User - -type QueryUsersConnection { - edges: [QueryUsersConnectionEdge!]! - pageInfo: PageInfo! -} - -type QueryUsersConnectionEdge { - cursor: String! - node: User! -} - -input QueryVariableInput { - owner: String! - slug: String! - variableName: String! -} - -union QueryVariableResult = BaseError | NotFoundError | Variable - -type ReactToGroupInviteResult { - invite: GroupInvite! - membership: UserGroupMembership -} - -type RelativeValuesCluster { - color: String! - id: String! - recommendedUnit: String -} - -input RelativeValuesClusterInput { - color: String! - id: String! - recommendedUnit: String -} - -type RelativeValuesDefinition implements Node { - createdAtTimestamp: Float! - currentRevision: RelativeValuesDefinitionRevision! - id: ID! - isEditable: Boolean! - modelExports: [RelativeValuesExport!]! - owner: Owner! - slug: String! - updatedAtTimestamp: Float! -} - -type RelativeValuesDefinitionConnection { - edges: [RelativeValuesDefinitionEdge!]! - pageInfo: PageInfo! -} - -type RelativeValuesDefinitionEdge { - cursor: String! - node: RelativeValuesDefinition! -} - -type RelativeValuesDefinitionRevision implements Node { - clusters: [RelativeValuesCluster!]! - id: ID! - items: [RelativeValuesItem!]! - recommendedUnit: String - title: String! -} - -input RelativeValuesDefinitionsQueryInput { - owner: String - slugContains: String -} - -type RelativeValuesExport implements Node { - cache: [RelativeValuesPairCache!]! - definition: RelativeValuesDefinition! - id: ID! - modelRevision: ModelRevision! - variableName: String! -} - -input RelativeValuesExportInput { - definition: DefinitionRefInput! - variableName: String! -} - -type RelativeValuesItem { - clusterId: String - description: String! - id: String! - name: String! -} - -input RelativeValuesItemInput { - clusterId: String - description: String - id: String! - name: String! -} - -type RelativeValuesPairCache implements Node { - errorString: String - firstItem: String! - id: ID! - resultJSON: String! - secondItem: String! -} - -type SearchEdge { - cursor: String! - node: Searchable! - rank: Float! - slugSnippet: String! - textSnippet: String! -} - -type Searchable implements Node { - id: ID! - link: String! - object: SearchableObject! -} - -union SearchableObject = Group | Model | RelativeValuesDefinition | User - -type SquiggleErrorOutput implements SquiggleOutput { - errorString: String! - isCached: Boolean! -} - -type SquiggleOkOutput implements SquiggleOutput { - bindingsJSON: String! - isCached: Boolean! - resultJSON: String! -} - -interface SquiggleOutput { - isCached: Boolean! -} - -type SquiggleSnippet implements Node { - autorunMode: Boolean - code: String! - id: ID! - sampleCount: Int - seed: String! - version: String! - xyPointLength: Int -} - -input SquiggleSnippetContentInput { - autorunMode: Boolean - code: String! - sampleCount: Int - seed: String! - version: String! - xyPointLength: Int -} - -type UpdateGroupInviteRoleResult { - invite: GroupInvite! -} - -type UpdateMembershipRoleResult { - membership: UserGroupMembership! -} - -type UpdateModelPrivacyResult { - model: Model! -} - -type UpdateModelSlugResult { - model: Model! -} - -type UpdateRelativeValuesDefinitionResult { - definition: RelativeValuesDefinition! -} - -type UpdateSquiggleSnippetResult { - model: Model! -} - -type User implements Node & Owner { - groups(after: String, before: String, first: Int, last: Int): GroupConnection! - id: ID! - isRoot: Boolean! - models(after: String, before: String, first: Int, last: Int): ModelConnection! - relativeValuesDefinitions(after: String, before: String, first: Int, last: Int): RelativeValuesDefinitionConnection! - slug: String! - username: String! - variables(after: String, before: String, first: Int, last: Int): VariableConnection! -} - -type UserGroupInvite implements GroupInvite & Node { - group: Group! - id: ID! - role: MembershipRole! - user: User! -} - -type UserGroupMembership implements Node { - group: Group! - id: ID! - role: MembershipRole! - user: User! -} - -type UserGroupMembershipConnection { - edges: [UserGroupMembershipEdge!]! - pageInfo: PageInfo! -} - -type UserGroupMembershipEdge { - cursor: String! - node: UserGroupMembership! -} - -input UsersQueryInput { - usernameContains: String -} - -type ValidateReusableGroupInviteTokenResult { - ok: Boolean! -} - -type ValidationError implements Error { - issues: [ValidationErrorIssue!]! - message: String! -} - -type ValidationErrorIssue { - message: String! - path: [String!]! -} - -type Variable implements Node { - currentRevision: VariableRevision - id: ID! - model: Model! - owner: Owner! - revisions(after: String, before: String, first: Int, last: Int): VariableRevisionConnection! - variableName: String! -} - -type VariableConnection { - edges: [VariableEdge!]! - pageInfo: PageInfo! -} - -type VariableEdge { - cursor: String! - node: Variable! -} - -input VariableQueryInput { - modelId: String - owner: String - variableName: String - variableType: String -} - -type VariableRevision implements Node { - docstring: String! - id: ID! - modelRevision: ModelRevision! - title: String - variable: Variable! - variableName: String! - variableType: String! -} - -type VariableRevisionConnection { - edges: [VariableRevisionEdge!]! - pageInfo: PageInfo! -} - -type VariableRevisionEdge { - cursor: String! - node: VariableRevision! -} \ No newline at end of file diff --git a/packages/hub/src/__generated__/.gitkeep b/packages/hub/src/__generated__/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/hub/src/server/ai/analytics/index.ts b/packages/hub/src/ai/data/analytics.ts similarity index 93% rename from packages/hub/src/server/ai/analytics/index.ts rename to packages/hub/src/ai/data/analytics.ts index 9e592cd4df..823365e2b1 100644 --- a/packages/hub/src/server/ai/analytics/index.ts +++ b/packages/hub/src/ai/data/analytics.ts @@ -1,13 +1,11 @@ -import "server-only"; - import * as Prisma from "@prisma/client"; import { CodeArtifact, Workflow } from "@quri/squiggle-ai/server"; -import { prisma } from "@/prisma"; -import { getAiCodec } from "@/server/ai/utils"; -import { v2WorkflowDataSchema } from "@/server/ai/v2_0"; -import { checkRootUser } from "@/server/helpers"; +import { getAiCodec } from "@/ai/data/utils"; +import { v2WorkflowDataSchema } from "@/ai/data/v2_0"; +import { prisma } from "@/lib/server/prisma"; +import { checkRootUser } from "@/users/auth"; async function loadWorkflows() { await checkRootUser(); diff --git a/packages/hub/src/server/ai/data.ts b/packages/hub/src/ai/data/loadWorkflows.ts similarity index 85% rename from packages/hub/src/server/ai/data.ts rename to packages/hub/src/ai/data/loadWorkflows.ts index e8d400c750..c5e7c20232 100644 --- a/packages/hub/src/server/ai/data.ts +++ b/packages/hub/src/ai/data/loadWorkflows.ts @@ -1,8 +1,8 @@ import "server-only"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; +import { getSessionUserOrRedirect } from "@/users/auth"; -import { getSessionUserOrRedirect } from "../helpers"; import { decodeDbWorkflowToClientWorkflow } from "./storage"; export async function loadWorkflows({ diff --git a/packages/hub/src/server/ai/storage.ts b/packages/hub/src/ai/data/storage.ts similarity index 88% rename from packages/hub/src/server/ai/storage.ts rename to packages/hub/src/ai/data/storage.ts index b85050db48..8172ea9e1e 100644 --- a/packages/hub/src/server/ai/storage.ts +++ b/packages/hub/src/ai/data/storage.ts @@ -4,8 +4,8 @@ import { AiWorkflow as PrismaAiWorkflow } from "@prisma/client"; import { ClientWorkflow } from "@quri/squiggle-ai"; -import { decodeV1_0JsonToClientWorkflow } from "@/server/ai/v1_0"; -import { decodeV2_0JsonToClientWorkflow } from "@/server/ai/v2_0"; +import { decodeV1_0JsonToClientWorkflow } from "@/ai/data/v1_0"; +import { decodeV2_0JsonToClientWorkflow } from "@/ai/data/v2_0"; export function decodeDbWorkflowToClientWorkflow( row: PrismaAiWorkflow diff --git a/packages/hub/src/server/ai/utils.ts b/packages/hub/src/ai/data/utils.ts similarity index 100% rename from packages/hub/src/server/ai/utils.ts rename to packages/hub/src/ai/data/utils.ts diff --git a/packages/hub/src/server/ai/v1_0.ts b/packages/hub/src/ai/data/v1_0.ts similarity index 100% rename from packages/hub/src/server/ai/v1_0.ts rename to packages/hub/src/ai/data/v1_0.ts diff --git a/packages/hub/src/server/ai/v2_0.ts b/packages/hub/src/ai/data/v2_0.ts similarity index 100% rename from packages/hub/src/server/ai/v2_0.ts rename to packages/hub/src/ai/data/v2_0.ts diff --git a/packages/hub/src/app/(frontpage)/definitions/page.tsx b/packages/hub/src/app/(frontpage)/definitions/page.tsx new file mode 100644 index 0000000000..50efc9ad56 --- /dev/null +++ b/packages/hub/src/app/(frontpage)/definitions/page.tsx @@ -0,0 +1,10 @@ +import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; +import { loadDefinitionCards } from "@/relative-values/data/cards"; + +export default async function DefinitionsPage() { + const page = await loadDefinitionCards(); + + return ; +} + +export const dynamic = "force-dynamic"; diff --git a/packages/hub/src/app/(frontpage)/groups/page.tsx b/packages/hub/src/app/(frontpage)/groups/page.tsx new file mode 100644 index 0000000000..7867e1cc62 --- /dev/null +++ b/packages/hub/src/app/(frontpage)/groups/page.tsx @@ -0,0 +1,10 @@ +import { GroupList } from "@/groups/components/GroupList"; +import { loadGroupCards } from "@/groups/data/groupCards"; + +export default async function OuterGroupsPage() { + const page = await loadGroupCards(); + + return ; +} + +export const dynamic = "force-dynamic"; diff --git a/packages/hub/src/app/(frontpage)/layout.tsx b/packages/hub/src/app/(frontpage)/layout.tsx new file mode 100644 index 0000000000..0a8265b387 --- /dev/null +++ b/packages/hub/src/app/(frontpage)/layout.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from "react"; + +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { + StyledTabLink, + StyledTabLinkList, +} from "@/components/ui/StyledTabLink"; +import { definitionsRoute, groupsRoute, variablesRoute } from "@/lib/routes"; + +export default function FrontPageLayout({ children }: PropsWithChildren) { + return ( + + + + + + + +
{children}
+
+ ); +} diff --git a/packages/hub/src/app/(frontpage)/loading.tsx b/packages/hub/src/app/(frontpage)/loading.tsx new file mode 100644 index 0000000000..702e460f6c --- /dev/null +++ b/packages/hub/src/app/(frontpage)/loading.tsx @@ -0,0 +1,5 @@ +import Skeleton from "react-loading-skeleton"; + +export default function Loading() { + return ; +} diff --git a/packages/hub/src/app/(frontpage)/page.tsx b/packages/hub/src/app/(frontpage)/page.tsx new file mode 100644 index 0000000000..b5f84bf8a4 --- /dev/null +++ b/packages/hub/src/app/(frontpage)/page.tsx @@ -0,0 +1,8 @@ +import { ModelList } from "@/models/components/ModelList"; +import { loadModelCards } from "@/models/data/cards"; + +export default async function FrontPage() { + const page = await loadModelCards(); + + return ; +} diff --git a/packages/hub/src/app/(frontpage)/variables/page.tsx b/packages/hub/src/app/(frontpage)/variables/page.tsx new file mode 100644 index 0000000000..2f5b99954d --- /dev/null +++ b/packages/hub/src/app/(frontpage)/variables/page.tsx @@ -0,0 +1,8 @@ +import { VariableList } from "@/variables/components/VariableList"; +import { loadVariableCards } from "@/variables/data/variableCards"; + +export default async function OuterVariablesPage() { + const variables = await loadVariableCards(); + + return ; +} diff --git a/packages/hub/src/app/FrontPage.tsx b/packages/hub/src/app/FrontPage.tsx deleted file mode 100644 index 79df9c5439..0000000000 --- a/packages/hub/src/app/FrontPage.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { StyledTab } from "@quri/ui"; - -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { FrontPageDefinitionList } from "./FrontPageDefinitionList"; -import { FrontPageGroupList } from "./FrontPageGroupList"; -import { FrontPageModelList } from "./FrontPageModelList"; -import { FrontPageVariableList } from "./FrontPageVariableList"; - -import { FrontPageQuery } from "@/__generated__/FrontPageQuery.graphql"; - -const Query = graphql` - query FrontPageQuery { - ...FrontPageModelList - ...FrontPageVariableList - ...FrontPageDefinitionList - ...FrontPageGroupList - } -`; - -export const FrontPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [data] = usePageQuery(Query, query); - - return ( - - - - - - - -
- - - - - - - - - - - - - - -
-
- ); -}; diff --git a/packages/hub/src/app/FrontPageDefinitionList.tsx b/packages/hub/src/app/FrontPageDefinitionList.tsx deleted file mode 100644 index 680e186fd1..0000000000 --- a/packages/hub/src/app/FrontPageDefinitionList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; - -import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; - -import { FrontPageDefinitionList$key } from "@/__generated__/FrontPageDefinitionList.graphql"; -import { FrontPageDefinitionListPaginationQuery } from "@/__generated__/FrontPageDefinitionListPaginationQuery.graphql"; - -const Fragment = graphql` - fragment FrontPageDefinitionList on Query - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "FrontPageDefinitionListPaginationQuery") { - relativeValuesDefinitions(first: $count, after: $cursor) - @connection(key: "FrontPageDefinitionList_relativeValuesDefinitions") { - # necessary for Relay - edges { - __typename - } - ...RelativeValuesDefinitionList - } - } -`; - -type Props = { - dataRef: FrontPageDefinitionList$key; -}; - -export const FrontPageDefinitionList: FC = ({ dataRef }) => { - const { - data: { relativeValuesDefinitions }, - loadNext, - } = usePaginationFragment< - FrontPageDefinitionListPaginationQuery, - FrontPageDefinitionList$key - >(Fragment, dataRef); - - return ( - - ); -}; diff --git a/packages/hub/src/app/FrontPageGroupList.tsx b/packages/hub/src/app/FrontPageGroupList.tsx deleted file mode 100644 index efff1707c4..0000000000 --- a/packages/hub/src/app/FrontPageGroupList.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; - -import { GroupList } from "@/groups/components/GroupList"; - -import { FrontPageGroupList$key } from "@/__generated__/FrontPageGroupList.graphql"; -import { FrontPageGroupListPaginationQuery } from "@/__generated__/FrontPageGroupListPaginationQuery.graphql"; - -const Fragment = graphql` - fragment FrontPageGroupList on Query - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "FrontPageGroupListPaginationQuery") { - groups(first: $count, after: $cursor) - @connection(key: "FrontPageGroupList_groups") { - edges { - __typename - } - ...GroupList - } - } -`; - -type Props = { - dataRef: FrontPageGroupList$key; -}; - -export const FrontPageGroupList: FC = ({ dataRef }) => { - const { - data: { groups }, - loadNext, - } = usePaginationFragment< - FrontPageGroupListPaginationQuery, - FrontPageGroupList$key - >(Fragment, dataRef); - - return ; -}; diff --git a/packages/hub/src/app/FrontPageModelList.tsx b/packages/hub/src/app/FrontPageModelList.tsx deleted file mode 100644 index 1f37ef31f9..0000000000 --- a/packages/hub/src/app/FrontPageModelList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; - -import { ModelList } from "@/models/components/ModelList"; - -import { FrontPageModelList$key } from "@/__generated__/FrontPageModelList.graphql"; -import { FrontPageModelListPaginationQuery } from "@/__generated__/FrontPageModelListPaginationQuery.graphql"; - -const Fragment = graphql` - fragment FrontPageModelList on Query - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 8 } - ) - @refetchable(queryName: "FrontPageModelListPaginationQuery") { - models(first: $count, after: $cursor) - @connection(key: "FrontPageModelList_models") { - # necessary for Relay - edges { - __typename - } - ...ModelList - } - } -`; - -type Props = { - dataRef: FrontPageModelList$key; -}; - -export const FrontPageModelList: FC = ({ dataRef }) => { - const { - data: { models }, - loadNext, - } = usePaginationFragment< - FrontPageModelListPaginationQuery, - FrontPageModelList$key - >(Fragment, dataRef); - - return ( - - ); -}; diff --git a/packages/hub/src/app/FrontPageVariableList.tsx b/packages/hub/src/app/FrontPageVariableList.tsx deleted file mode 100644 index d31749e805..0000000000 --- a/packages/hub/src/app/FrontPageVariableList.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; - -import { VariableList } from "@/variables/components/VariableList"; - -import { FrontPageVariableList$key } from "@/__generated__/FrontPageVariableList.graphql"; -import { FrontPageVariableListPaginationQuery } from "@/__generated__/FrontPageVariableListPaginationQuery.graphql"; - -const Fragment = graphql` - fragment FrontPageVariableList on Query - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "FrontPageVariableListPaginationQuery") { - variables(first: $count, after: $cursor) - @connection(key: "FrontPageVariableList_variables") { - edges { - __typename - } - ...VariableList - } - } -`; - -type Props = { - dataRef: FrontPageVariableList$key; -}; - -export const FrontPageVariableList: FC = ({ dataRef }) => { - const { - data: { variables }, - loadNext, - } = usePaginationFragment< - FrontPageVariableListPaginationQuery, - FrontPageVariableList$key - >(Fragment, dataRef); - - return ; -}; diff --git a/packages/hub/src/app/RootLayout.tsx b/packages/hub/src/app/RootLayout.tsx deleted file mode 100644 index 0243156b87..0000000000 --- a/packages/hub/src/app/RootLayout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; -import { Session } from "next-auth"; -import { useSession } from "next-auth/react"; -import { usePathname } from "next/navigation"; -import { FC, PropsWithChildren } from "react"; -import { useLazyLoadQuery } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { isAiRoute, isModelRoute } from "@/routes"; - -import { PageFooter } from "../components/layout/RootLayout/PageFooter"; -import { PageMenu } from "../components/layout/RootLayout/PageMenu"; -import { ReactRoot } from "../components/ReactRoot"; - -import { RootLayoutQuery } from "@/__generated__/RootLayoutQuery.graphql"; - -const InnerRootLayout: FC = ({ children }) => { - const { data: session } = useSession(); - const queryData = useLazyLoadQuery( - graphql` - query RootLayoutQuery($signedIn: Boolean!) { - ...PageMenu @arguments(signedIn: $signedIn) - } - `, - { signedIn: !!session } - ); - - const pathname = usePathname(); - - const showFooter = !isModelRoute(pathname) && !isAiRoute(pathname); - - return ( -
- -
- {children} -
- {showFooter && } -
- ); -}; - -export const RootLayout: FC< - PropsWithChildren<{ - session: Session | null; - }> -> = ({ session, children }) => { - return ( - - {children} - - ); -}; diff --git a/packages/hub/src/app/about/page.tsx b/packages/hub/src/app/about/page.tsx index 61b6cc254e..5932e0a4ab 100644 --- a/packages/hub/src/app/about/page.tsx +++ b/packages/hub/src/app/about/page.tsx @@ -9,8 +9,7 @@ import { GITHUB_URL, NEWSLETTER_URL, QURI_DONATE_URL, -} from "@/lib/common"; -import { graphqlPlaygroundRoute } from "@/routes"; +} from "@/lib/constants"; const markdown = ` # About Squiggle Hub @@ -27,7 +26,6 @@ Squiggle Hub is made by the [Quantified Uncertainty Research Institute](https:// ## Key Links - [Squiggle](https://www.squiggle-language.com/) -- [Squiggle API](${graphqlPlaygroundRoute()}) - [Squiggle Discord](${DISCORD_URL}) - [Squiggle Github](${GITHUB_URL}) - [Squiggle Github Discussion (For Ideas and Issues)](${GITHUB_DISCUSSION_URL}) @@ -36,9 +34,6 @@ Squiggle Hub is made by the [Quantified Uncertainty Research Institute](https:// ## Licensing Squiggle Hub, along with Squiggle, is available for free use, with the code being open-source and licensed under MIT. Access it [here](https://github.com/quantified-uncertainty/squiggle). -## Squiggle Hub API -Developed using a GraphQL API, Squiggle Hub can be accessed [here](${graphqlPlaygroundRoute()}). The API is recommended for querying. If you want to run mutations, reach out to us via [Discord](${DISCORD_URL}). Squiggle's JavaScript implementation is available on [NPM](https://www.npmjs.com/package/squiggle-lang). There are several [integrations](https://www.squiggle-language.com/docs/Integrations) like VS Code and Observable. - ## Feature: Relative Values Squiggle Hub currently supports experimental [relative values](https://forum.effectivealtruism.org/posts/EFEwBvuDrTLDndqCt/relative-value-functions-a-flexible-new-format-for-value), with future improvements planned. diff --git a/packages/hub/src/app/admin/RebuildSearchIndex.tsx b/packages/hub/src/app/admin/RebuildSearchIndex.tsx deleted file mode 100644 index d39e89aaa2..0000000000 --- a/packages/hub/src/app/admin/RebuildSearchIndex.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { RebuildSearchIndexMutation } from "@/__generated__/RebuildSearchIndexMutation.graphql"; -import { H2 } from "@/components/ui/Headers"; -import { MutationButton } from "@/components/ui/MutationButton"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -export const RebuildSearchIndex: FC = () => { - return ( -
-

Rebuild search index

- - mutation={graphql` - mutation RebuildSearchIndexMutation { - result: adminRebuildSearchIndex { - __typename - ... on BaseError { - message - } - ... on AdminRebuildSearchIndexResult { - ok - } - } - } - `} - expectedTypename="AdminRebuildSearchIndexResult" - title="Rebuild" - confirmation="Index updated" - theme="primary" - variables={{}} - /> -
- ); -}; diff --git a/packages/hub/src/app/admin/dev/page.tsx b/packages/hub/src/app/admin/dev/page.tsx index fb07951d34..920a19cf1b 100644 --- a/packages/hub/src/app/admin/dev/page.tsx +++ b/packages/hub/src/app/admin/dev/page.tsx @@ -4,7 +4,7 @@ import { Button } from "@quri/ui"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { H2 } from "@/components/ui/Headers"; -import { resetPrisma } from "@/prisma"; +import { resetPrisma } from "@/lib/server/prisma"; export default async function () { if (process.env.NODE_ENV !== "development") { diff --git a/packages/hub/src/app/admin/layout.tsx b/packages/hub/src/app/admin/layout.tsx index c20592c791..3649116577 100644 --- a/packages/hub/src/app/admin/layout.tsx +++ b/packages/hub/src/app/admin/layout.tsx @@ -2,11 +2,11 @@ import { PropsWithChildren } from "react"; import { LockIcon } from "@quri/ui"; -import { auth } from "@/auth"; import { FullLayoutWithPadding } from "@/components/layout/FullLayoutWithPadding"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { H1 } from "@/components/ui/Headers"; -import { isRootEmail } from "@/graphql/helpers/userHelpers"; +import { auth } from "@/lib/server/auth"; +import { isRootEmail } from "@/users/auth"; export default async function AdminLayout({ children }: PropsWithChildren) { const session = await auth(); diff --git a/packages/hub/src/app/admin/search/page.tsx b/packages/hub/src/app/admin/search/page.tsx new file mode 100644 index 0000000000..72f60e9c02 --- /dev/null +++ b/packages/hub/src/app/admin/search/page.tsx @@ -0,0 +1,21 @@ +import { H2 } from "@/components/ui/Headers"; +import { SafeActionButton } from "@/components/ui/SafeActionButton"; +import { adminRebuildSearchIndexAction } from "@/search/actions/adminRebuildSearchIndexAction"; +import { checkRootUser } from "@/users/auth"; + +export default async function AdminSearchPage() { + await checkRootUser(); + + return ( +
+

Rebuild search index

+ +
+ ); +} diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx index 31b8f6ba6d..1e8743e8ce 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx @@ -1,7 +1,6 @@ "use client"; +import { useRouter } from "next/navigation"; import { FC, useState } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { Button, @@ -12,36 +11,18 @@ import { import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; import { H2 } from "@/components/ui/Headers"; -import { MutationButton } from "@/components/ui/MutationButton"; +import { SafeActionButton } from "@/components/ui/SafeActionButton"; import { StyledLink } from "@/components/ui/StyledLink"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { modelRoute } from "@/routes"; +import { modelRoute } from "@/lib/routes"; +import { adminUpdateModelVersionAction } from "@/models/actions/adminUpdateModelVersionAction"; +import { ModelByVersion } from "@/models/data/byVersion"; import { UpgradeableModel } from "./UpgradeableModel"; -import { UpgradeVersionsPage_List$key } from "@/__generated__/UpgradeVersionsPage_List.graphql"; -import { UpgradeVersionsPage_updateMutation } from "@/__generated__/UpgradeVersionsPage_updateMutation.graphql"; -import { UpgradeVersionsPageQuery } from "@/__generated__/UpgradeVersionsPageQuery.graphql"; - const ModelList: FC<{ - modelsRef: UpgradeVersionsPage_List$key; - reload: () => void; -}> = ({ modelsRef, reload }) => { - const models = useFragment( - graphql` - fragment UpgradeVersionsPage_List on Model @relay(plural: true) { - id - slug - owner { - id - slug - } - ...UpgradeableModel_Ref - } - `, - modelsRef - ); + models: ModelByVersion["models"]; +}> = ({ models }) => { + const router = useRouter(); const [pos, setPos] = useState(0); @@ -63,40 +44,13 @@ const ModelList: FC<{ {model.owner.slug}/{model.slug} - - expectedTypename="AdminUpdateModelVersionResult" - mutation={graphql` - mutation UpgradeVersionsPage_updateMutation( - $input: MutationAdminUpdateModelVersionInput! - ) { - result: adminUpdateModelVersion(input: $input) { - __typename - ... on BaseError { - message - } - ... on AdminUpdateModelVersionResult { - model { - ...EditSquiggleSnippetModel - } - } - } - } - `} - updater={(store) => { - // reload() from usePageQuery doesn't work for some reason - store.get(model.id)?.invalidateRecord(); - reload(); - // window.location.reload(); - }} - variables={{ - input: { - modelId: model.id, - version: defaultSquiggleVersion, - }, + router.refresh()} title={`Upgrade to ${defaultSquiggleVersion}`} theme="primary" /> @@ -110,32 +64,15 @@ const ModelList: FC<{ Next → - + ); }; export const UpgradeVersionsPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - // TODO - this fetches all models even if we show just one, can we optimize it? - const [{ modelsByVersion }, { reload }] = usePageQuery( - graphql` - query UpgradeVersionsPageQuery { - modelsByVersion { - version - count - privateCount - models { - ...UpgradeVersionsPage_List - } - } - } - `, - query - ); - - type Entry = (typeof upgradeableModelsByVersion)[number]; + modelsByVersion: ModelByVersion[]; +}> = ({ modelsByVersion }) => { + type Entry = (typeof modelsByVersion)[number]; const upgradeableModelsByVersion = modelsByVersion.filter( (entry) => @@ -206,9 +143,7 @@ export const UpgradeVersionsPage: FC<{ - {selectedEntry ? ( - - ) : null} + {selectedEntry ? : null} ); }; diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx index 6fe6f3d8e4..0591fc28ab 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx @@ -1,7 +1,6 @@ "use client"; -import { FC, use } from "react"; -import { useFragment, useLazyLoadQuery } from "react-relay"; -import { graphql } from "relay-runtime"; +import { FC, use, useEffect, useState } from "react"; +import Skeleton from "react-loading-skeleton"; import { defaultSquiggleVersion, @@ -11,69 +10,21 @@ import { } from "@quri/versioned-squiggle-components"; import { EditSquiggleSnippetModel } from "@/app/models/[owner]/[slug]/EditSquiggleSnippetModel"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; - -import { UpgradeableModel_Ref$key } from "@/__generated__/UpgradeableModel_Ref.graphql"; -import { UpgradeableModelQuery } from "@/__generated__/UpgradeableModelQuery.graphql"; - -export const UpgradeableModel: FC<{ - modelRef: UpgradeableModel_Ref$key; -}> = ({ modelRef }) => { - const incompleteModel = useFragment( - graphql` - fragment UpgradeableModel_Ref on Model { - id - slug - owner { - id - slug - } - } - `, - modelRef - ); - - const result = useLazyLoadQuery( - graphql` - query UpgradeableModelQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - id - currentRevision { - content { - __typename - ... on SquiggleSnippet { - id - code - version - seed - } - } - } - ...EditSquiggleSnippetModel - } - } - } - `, - { - input: { - slug: incompleteModel.slug, - owner: incompleteModel.owner.slug, - }, - } - ); - - const model = extractFromGraphqlErrorUnion(result.model, "Model"); +import { loadModelFullAction } from "@/models/actions/loadModelFullAction"; +import { ModelByVersion } from "@/models/data/byVersion"; +import { ModelFullDTO } from "@/models/data/full"; +import { sqProjectWithHubLinker } from "@/squiggle/linker"; +const InnerUpgradeableModel: FC<{ + model: ModelFullDTO; +}> = ({ model }) => { const currentRevision = model.currentRevision; - if (currentRevision.content.__typename !== "SquiggleSnippet") { - throw new Error("Wrong content type"); - } + const code = currentRevision.squiggleSnippet.code; - const version = useAdjustSquiggleVersion(currentRevision.content.version); + const version = useAdjustSquiggleVersion( + currentRevision.squiggleSnippet.version + ); const updatedVersion = defaultSquiggleVersion; const squiggle = use(versionedSquigglePackages(version)); @@ -90,12 +41,9 @@ export const UpgradeableModel: FC<{
{version}
{updatedVersion}
- +
@@ -104,9 +52,42 @@ export const UpgradeableModel: FC<{ return ( ); } }; + +export const UpgradeableModel: FC<{ + model: ModelByVersion["models"][number]; +}> = ({ model: incompleteModel }) => { + const [model, setModel] = useState( + "loading" + ); + + useEffect(() => { + // TODO - this is done with a server action, so it's not cached. + // A route would be better. + loadModelFullAction({ + owner: incompleteModel.owner.slug, + slug: incompleteModel.slug, + }).then((result) => { + if (result?.data) { + setModel(result.data); + } else { + setModel(null); + } + }); + }, [incompleteModel]); + + if (model === "loading") { + return ; + } + + if (!model) { + return
Model not found
; + } + + return ; +}; diff --git a/packages/hub/src/app/admin/upgrade-versions/page.tsx b/packages/hub/src/app/admin/upgrade-versions/page.tsx index 64291265e5..b864a5e625 100644 --- a/packages/hub/src/app/admin/upgrade-versions/page.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/page.tsx @@ -1,18 +1,17 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { loadModelsByVersion } from "@/models/data/byVersion"; +import { checkRootUser } from "@/users/auth"; import { UpgradeVersionsPage } from "./UpgradeVersionsPage"; -import QueryNode, { - UpgradeVersionsPageQuery, -} from "@/__generated__/UpgradeVersionsPageQuery.graphql"; - export default async function OuterUpgradeVersionsPage() { - // permissions are checked in ./layout.tsx - const query = await loadPageQuery(QueryNode, {}); + await checkRootUser(); + + // TODO - this fetches all models even if we show just one, can we optimize it? + const data = await loadModelsByVersion(); - return ; + return ; } export const metadata: Metadata = { diff --git a/packages/hub/src/app/ai/WorkflowViewer/ClientStepView.tsx b/packages/hub/src/app/ai/WorkflowViewer/ClientStepView.tsx index 4988961395..be2400a713 100644 --- a/packages/hub/src/app/ai/WorkflowViewer/ClientStepView.tsx +++ b/packages/hub/src/app/ai/WorkflowViewer/ClientStepView.tsx @@ -4,7 +4,7 @@ import { FC, useMemo } from "react"; import { ClientArtifact, ClientStep } from "@quri/squiggle-ai"; import { ChevronLeftIcon, ChevronRightIcon } from "@quri/ui"; -import { useAvailableHeight } from "@/hooks/useAvailableHeight"; +import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; import { SquigglePlaygroundForWorkflow } from "../SquigglePlaygroundForWorkflow"; import { stepNames } from "../utils"; diff --git a/packages/hub/src/app/ai/WorkflowViewer/index.tsx b/packages/hub/src/app/ai/WorkflowViewer/index.tsx index d48ac9949b..2b5e60c035 100644 --- a/packages/hub/src/app/ai/WorkflowViewer/index.tsx +++ b/packages/hub/src/app/ai/WorkflowViewer/index.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { ClientWorkflow } from "@quri/squiggle-ai"; import { StyledTab } from "@quri/ui"; -import { useAvailableHeight } from "@/hooks/useAvailableHeight"; +import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; import { LogsView } from "../LogsView"; import { SquigglePlaygroundForWorkflow } from "../SquigglePlaygroundForWorkflow"; diff --git a/packages/hub/src/app/ai/analytics/StepErrorList.tsx b/packages/hub/src/app/ai/analytics/StepErrorList.tsx index d59575761f..18f8cc9aee 100644 --- a/packages/hub/src/app/ai/analytics/StepErrorList.tsx +++ b/packages/hub/src/app/ai/analytics/StepErrorList.tsx @@ -1,7 +1,7 @@ import { FC, Fragment } from "react"; +import { type StepError } from "@/ai/data/analytics"; import { H2 } from "@/components/ui/Headers"; -import { type StepError } from "@/server/ai/analytics"; export const StepErrorList: FC<{ errors: StepError[]; diff --git a/packages/hub/src/app/ai/analytics/code-errors/page.tsx b/packages/hub/src/app/ai/analytics/code-errors/page.tsx index 8ffbe112f2..4b89a6ff7e 100644 --- a/packages/hub/src/app/ai/analytics/code-errors/page.tsx +++ b/packages/hub/src/app/ai/analytics/code-errors/page.tsx @@ -1,4 +1,4 @@ -import { getCodeErrors } from "@/server/ai/analytics"; +import { getCodeErrors } from "@/ai/data/analytics"; import { StepErrorList } from "../StepErrorList"; diff --git a/packages/hub/src/app/ai/analytics/layout.tsx b/packages/hub/src/app/ai/analytics/layout.tsx index 0f077a0528..f591cdca79 100644 --- a/packages/hub/src/app/ai/analytics/layout.tsx +++ b/packages/hub/src/app/ai/analytics/layout.tsx @@ -1,10 +1,13 @@ import { PropsWithChildren } from "react"; -import { checkRootUser } from "@/server/helpers"; +import { WithAuth } from "@/components/WithAuth"; import { AiAnalyticsClientLayout } from "./ClientLayout"; export default async function ({ children }: PropsWithChildren) { - await checkRootUser(); - return {children}; + return ( + + {children} + + ); } diff --git a/packages/hub/src/app/ai/analytics/page.tsx b/packages/hub/src/app/ai/analytics/page.tsx index cf13a29386..33c25b1534 100644 --- a/packages/hub/src/app/ai/analytics/page.tsx +++ b/packages/hub/src/app/ai/analytics/page.tsx @@ -1,7 +1,7 @@ import { Fragment } from "react"; +import { getTypeStats } from "@/ai/data/analytics"; import { H2 } from "@/components/ui/Headers"; -import { getTypeStats } from "@/server/ai/analytics"; export default async function () { const typeStats = await getTypeStats(); diff --git a/packages/hub/src/app/ai/analytics/step-errors/page.tsx b/packages/hub/src/app/ai/analytics/step-errors/page.tsx index 0ede8f95de..17750fcfdf 100644 --- a/packages/hub/src/app/ai/analytics/step-errors/page.tsx +++ b/packages/hub/src/app/ai/analytics/step-errors/page.tsx @@ -1,4 +1,4 @@ -import { getStepErrors } from "@/server/ai/analytics"; +import { getStepErrors } from "@/ai/data/analytics"; import { StepErrorList } from "../StepErrorList"; diff --git a/packages/hub/src/app/ai/api/create/route.ts b/packages/hub/src/app/ai/api/create/route.ts index 85b474bfd4..a3702062ca 100644 --- a/packages/hub/src/app/ai/api/create/route.ts +++ b/packages/hub/src/app/ai/api/create/route.ts @@ -7,10 +7,10 @@ import { Workflow, } from "@quri/squiggle-ai/server"; -import { auth } from "@/auth"; -import { getSelf, isSignedIn } from "@/graphql/helpers/userHelpers"; -import { prisma } from "@/prisma"; -import { workflowToV2_0Json } from "@/server/ai/v2_0"; +import { workflowToV2_0Json } from "@/ai/data/v2_0"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { getSelf, isSignedIn } from "@/users/auth"; import { AiRequestBody, aiRequestBodySchema } from "../../utils"; diff --git a/packages/hub/src/app/ai/page.tsx b/packages/hub/src/app/ai/page.tsx index 2f7ea6331a..b64761300e 100644 --- a/packages/hub/src/app/ai/page.tsx +++ b/packages/hub/src/app/ai/page.tsx @@ -1,7 +1,7 @@ import { z } from "zod"; +import { loadWorkflows } from "@/ai/data/loadWorkflows"; import { numberInString } from "@/lib/zodUtils"; -import { loadWorkflows } from "@/server/ai/data"; import { AiDashboard } from "./AiDashboard"; diff --git a/packages/hub/src/app/api/auth/[...nextauth]/route.ts b/packages/hub/src/app/api/auth/[...nextauth]/route.ts index a49631a69f..dffc891aed 100644 --- a/packages/hub/src/app/api/auth/[...nextauth]/route.ts +++ b/packages/hub/src/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,3 @@ -import { handlers } from "../../../../auth"; +import { handlers } from "../../../../lib/server/auth"; export const { GET, POST } = handlers; diff --git a/packages/hub/src/app/api/find-owners/route.ts b/packages/hub/src/app/api/find-owners/route.ts new file mode 100644 index 0000000000..f720cf4a16 --- /dev/null +++ b/packages/hub/src/app/api/find-owners/route.ts @@ -0,0 +1,24 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { findOwnersForSelect } from "@/owners/data/findOwners"; + +// We're not calling this as a server actions because it'd be too slow (server actions are sequential). +// TODO: it'd be good to use tRPC for this. +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { search, mode } = z + .object({ + search: z.string(), + mode: z.enum(["all", "all-users", "all-groups", "my", "my-groups"]), + }) + .parse(Object.fromEntries(searchParams.entries())); + + return Response.json( + await findOwnersForSelect({ + search, + mode, + }) + ); +} diff --git a/packages/hub/src/app/api/find-relative-values/route.ts b/packages/hub/src/app/api/find-relative-values/route.ts new file mode 100644 index 0000000000..bbd40513b1 --- /dev/null +++ b/packages/hub/src/app/api/find-relative-values/route.ts @@ -0,0 +1,24 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { findRelativeValuesForSelect } from "@/relative-values/data/findRelativeValuesForSelect"; + +// We're not calling this as a server actions because it'd be too slow (server actions are sequential). +// TODO: it'd be good to use tRPC for this. +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { owner, slugContains } = z + .object({ + owner: z.string(), + slugContains: z.string(), + }) + .parse(Object.fromEntries(searchParams.entries())); + + return Response.json( + await findRelativeValuesForSelect({ + owner, + slugContains, + }) + ); +} diff --git a/packages/hub/src/app/api/get-group-models/route.ts b/packages/hub/src/app/api/get-group-models/route.ts new file mode 100644 index 0000000000..db0a4e0e84 --- /dev/null +++ b/packages/hub/src/app/api/get-group-models/route.ts @@ -0,0 +1,23 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { loadModelCards } from "@/models/data/cards"; + +// We're not calling this as a server actions because it'd be too slow (server actions are sequential). +// TODO: it'd be good to use tRPC for this. +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { slug } = z + .object({ + slug: z.string(), + }) + .parse(Object.fromEntries(searchParams.entries())); + + const page = await loadModelCards({ ownerSlug: slug, limit: 100 }); + + return Response.json({ + models: page.items, + hasMore: !!page.loadMore, + }); +} diff --git a/packages/hub/src/app/api/get-source/route.ts b/packages/hub/src/app/api/get-source/route.ts new file mode 100644 index 0000000000..efaaf427e7 --- /dev/null +++ b/packages/hub/src/app/api/get-source/route.ts @@ -0,0 +1,27 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { zSlug } from "@/lib/zodUtils"; +import { loadModelCard } from "@/models/data/cards"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { owner, slug } = z + .object({ + owner: zSlug, + slug: zSlug, + }) + .parse(Object.fromEntries(searchParams.entries())); + + const model = await loadModelCard({ owner, slug }); + if (!model?.currentRevision.squiggleSnippet) { + return Response.json({ error: "Not found" }, { status: 404 }); + } + + const code = model.currentRevision.squiggleSnippet.code; + + return Response.json({ + code, + }); +} diff --git a/packages/hub/src/app/api/graphql/route.ts b/packages/hub/src/app/api/graphql/route.ts deleted file mode 100644 index faad42fde6..0000000000 --- a/packages/hub/src/app/api/graphql/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createYoga } from "graphql-yoga"; -import { NextRequest, NextResponse } from "next/server"; - -import { auth } from "@/auth"; -import { schema } from "@/graphql/schema"; - -const yoga = createYoga({ - graphqlEndpoint: "/api/graphql", - schema, - context: async () => { - // There's some magic involved here; - // getServerSession() obtains request data through Next.js cookies() and headers() functions - // See also: https://github.com/nextauthjs/next-auth/issues/7355 - const session = await auth(); - return { session }; - }, -}); - -async function handler(request: NextRequest) { - const response = await yoga.fetch(request, { - method: request.method, - headers: request.headers, - body: request.body, - }); - return new NextResponse(response.body, { - headers: response.headers, - status: response.status, - }); -} - -export { handler as GET, handler as POST }; diff --git a/packages/hub/src/app/api/runSquiggle/route.ts b/packages/hub/src/app/api/runSquiggle/route.ts index 3cc422e217..538a3f2588 100644 --- a/packages/hub/src/app/api/runSquiggle/route.ts +++ b/packages/hub/src/app/api/runSquiggle/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { runSquiggleWithCache } from "@/graphql/queries/runSquiggle"; +import { runSquiggleWithCache } from "@/lib/server/runSquiggle"; export async function POST(req: NextRequest) { // Assuming 'code' is sent in the request body and is a string diff --git a/packages/hub/src/app/api/search/route.ts b/packages/hub/src/app/api/search/route.ts new file mode 100644 index 0000000000..8d3884086b --- /dev/null +++ b/packages/hub/src/app/api/search/route.ts @@ -0,0 +1,261 @@ +import { Searchable as PrismaSearchable } from "@prisma/client"; +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { + groupRoute, + modelRoute, + relativeValuesRoute, + userRoute, +} from "@/lib/routes"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; + +import { SearchResult } from "./schema"; + +type SearchableWithEdgeData = PrismaSearchable & { + rank: number; + slugSnippet: string; + textSnippet: string; +}; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { query } = z + .object({ + query: z.string(), + }) + .parse(Object.fromEntries(searchParams.entries())); + + const limit = 20; + const offset = 0; + + const session = await auth(); + + const rows = await prisma.$queryRaw` + SELECT + "Searchable".*, + -- ranking function + ts_rank_cd( + setweight(to_tsvector(coalesce("Model".slug, '')), 'A') || + setweight(to_tsvector(coalesce("SquiggleSnippet".code, '')), 'B') || + setweight(to_tsvector(coalesce("RelativeValuesDefinition".slug, '')), 'A') || + setweight(to_tsvector(coalesce("UserOwner".slug, '')), 'A') || + setweight(to_tsvector(coalesce("GroupOwner".slug, '')), 'A'), + websearch_to_tsquery(${query}) + ) AS rank, + ts_headline( + concat_ws( + ' ', + "Model".slug, + "RelativeValuesDefinition".slug, + "UserOwner".slug, + "GroupOwner".slug + ), + websearch_to_tsquery(${query}), + 'HighlightAll=t' + ) AS "slugSnippet", + ts_headline(concat_ws( + ' ', + "SquiggleSnippet".code + ), websearch_to_tsquery(${query}) + ) AS "textSnippet" + FROM "Searchable" + LEFT JOIN "Model" ON "Model".id = "Searchable"."modelId" + LEFT JOIN "Owner" AS "ModelOwner" ON "Model"."ownerId" = "ModelOwner".id + LEFT JOIN "User" AS "ModelOwnerUser" ON "ModelOwner".id = "ModelOwnerUser"."ownerId" + LEFT JOIN "Group" AS "ModelOwnerGroup" ON "ModelOwner".id = "ModelOwnerGroup"."ownerId" + LEFT JOIN "ModelRevision" ON "Model"."currentRevisionId" = "ModelRevision".id + LEFT JOIN "SquiggleSnippet" ON "ModelRevision"."contentId" = "SquiggleSnippet".id + LEFT JOIN "RelativeValuesDefinition" ON "RelativeValuesDefinition".id = "Searchable"."definitionId" + -- LEFT JOIN "Owner" AS "RelativeValuesDefinitionOwner" ON "RelativeValuesDefinition"."ownerId" = "RelativeValuesDefinitionOwner".id + LEFT JOIN "User" ON "User".id = "Searchable"."userId" + LEFT JOIN "Owner" AS "UserOwner" ON "User"."ownerId" = "UserOwner".id + LEFT JOIN "Group" ON "Group".id = "Searchable"."groupId" + LEFT JOIN "Owner" AS "GroupOwner" ON "Group"."ownerId" = "GroupOwner".id + WHERE + ( + -- check permissions, should match "modelWhereHasAccess" function + "Model".id IS NULL OR + "Model"."isPrivate" IS FALSE + OR "ModelOwnerUser".email = ${session?.user.email} + OR "ModelOwnerGroup".id IN ( + SELECT DISTINCT "groupId" + FROM "UserGroupMembership" + LEFT JOIN "User" ON "User".id = "UserGroupMembership"."userId" + WHERE "User".email = ${session?.user.email} + ) + ) AND ( + -- construct search document + to_tsvector(concat_ws( + ' ', + "Model".slug, + "SquiggleSnippet".code, + "RelativeValuesDefinition".slug, + "UserOwner".slug, + "GroupOwner".slug + )) @@ websearch_to_tsquery(${query}) + ) + ORDER BY rank DESC + LIMIT ${limit} + OFFSET ${offset} + `; + + const modelIds = rows + .filter( + (row): row is SearchableWithEdgeData & { modelId: string } => + row.modelId !== null + ) + .map((row) => row.modelId); + + const definitionIds = rows + .filter( + (row): row is SearchableWithEdgeData & { definitionId: string } => + row.definitionId !== null + ) + .map((row) => row.definitionId); + + const userIds = rows + .filter( + (row): row is SearchableWithEdgeData & { userId: string } => + row.userId !== null + ) + .map((row) => row.userId); + + const groupIds = rows + .filter( + (row): row is SearchableWithEdgeData & { groupId: string } => + row.groupId !== null + ) + .map((row) => row.groupId); + + const models = modelIds.length + ? await prisma.model.findMany({ + where: { + id: { in: modelIds }, + }, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }) + : []; + + const definitions = definitionIds.length + ? await prisma.relativeValuesDefinition.findMany({ + where: { + id: { in: definitionIds }, + }, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }) + : []; + + const users = userIds.length + ? await prisma.user.findMany({ + where: { + id: { in: userIds }, + }, + select: { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + }, + }) + : []; + + const groups = groupIds.length + ? await prisma.group.findMany({ + where: { + id: { in: groupIds }, + }, + select: { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + }, + }) + : []; + + const result: SearchResult = []; + + for (const row of rows) { + if (row.modelId !== null) { + const model = models.find((m) => m.id === row.modelId); + if (!model) continue; + + result.push({ + id: row.id, + link: modelRoute({ owner: model.owner.slug, slug: model.slug }), + slugSnippet: row.slugSnippet, + textSnippet: row.textSnippet, + object: { type: "Model", owner: model.owner.slug, slug: model.slug }, + }); + } else if (row.definitionId !== null) { + const definition = definitions.find((d) => d.id === row.definitionId); + if (!definition) continue; + + result.push({ + id: row.id, + link: relativeValuesRoute({ + owner: definition.owner.slug, + slug: definition.slug, + }), + slugSnippet: row.slugSnippet, + textSnippet: row.textSnippet, + object: { + type: "RelativeValuesDefinition", + owner: definition.owner.slug, + slug: definition.slug, + }, + }); + } else if (row.userId !== null) { + const user = users.find((u) => u.id === row.userId); + if (!user?.asOwner) continue; + + const username = user.asOwner.slug; + + result.push({ + id: row.id, + link: userRoute({ username }), + slugSnippet: row.slugSnippet, + textSnippet: row.textSnippet, + object: { type: "User", slug: username }, + }); + } else if (row.groupId !== null) { + const group = groups.find((g) => g.id === row.groupId); + if (!group?.asOwner) continue; + + const slug = group.asOwner.slug; + + result.push({ + id: row.id, + link: groupRoute({ slug }), + slugSnippet: row.slugSnippet, + textSnippet: row.textSnippet, + object: { type: "Group", slug }, + }); + } + } + + return Response.json(result); +} diff --git a/packages/hub/src/app/api/search/schema.ts b/packages/hub/src/app/api/search/schema.ts new file mode 100644 index 0000000000..8a462c450f --- /dev/null +++ b/packages/hub/src/app/api/search/schema.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +const searchResultObjectSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("Model"), + slug: z.string(), + owner: z.string(), + }), + z.object({ + type: z.literal("RelativeValuesDefinition"), + slug: z.string(), + owner: z.string(), + }), + z.object({ + type: z.literal("User"), + slug: z.string(), + }), + z.object({ + type: z.literal("Group"), + slug: z.string(), + }), +]); + +export const searchResultSchema = z.array( + z.object({ + id: z.string(), + link: z.string(), + slugSnippet: z.string(), + textSnippet: z.string(), + object: searchResultObjectSchema, + }) +); + +export type SearchResult = z.infer; + +export type SearchResultItem = SearchResult[number]; + +export type TypedSearchResultItem< + T extends SearchResultItem["object"]["type"], +> = SearchResultItem & { + object: Extract; +}; diff --git a/packages/hub/src/app/groups/[slug]/GroupLayout.tsx b/packages/hub/src/app/groups/[slug]/GroupLayout.tsx deleted file mode 100644 index 04c1ac5dfb..0000000000 --- a/packages/hub/src/app/groups/[slug]/GroupLayout.tsx +++ /dev/null @@ -1,100 +0,0 @@ -"use client"; -import { useRouter, useSelectedLayoutSegment } from "next/navigation"; -import { FC, PropsWithChildren } from "react"; -import { useSubscribeToInvalidationState } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { Button, GroupIcon, PlusIcon } from "@quri/ui"; - -import { H1 } from "@/components/ui/Headers"; -import { StyledTabLink } from "@/components/ui/StyledTabLink"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { groupMembersRoute, groupRoute, newModelRoute } from "@/routes"; - -import { useIsGroupMember } from "./hooks"; -import { InviteForMe } from "./InviteForMe"; - -import { GroupLayoutQuery } from "@/__generated__/GroupLayoutQuery.graphql"; - -const Query = graphql` - query GroupLayoutQuery($slug: String!) { - result: group(slug: $slug) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on Group { - id - slug - ...hooks_useIsGroupAdmin - ...hooks_useIsGroupMember - ...InviteForMe - } - } - } -`; - -const NewButton: FC<{ group: string }> = ({ group }) => { - const segment = useSelectedLayoutSegment(); - - let link = newModelRoute({ group }); - - const router = useRouter(); - - if (segment === "members" || segment === "invite-link") { - return null; - } - - return ( - - ); -}; - -export const GroupLayout: FC< - PropsWithChildren<{ - query: SerializablePreloadedQuery; - }> -> = ({ query, children }) => { - const [{ result }, { reload }] = usePageQuery(Query, query); - const group = extractFromGraphqlErrorUnion(result, "Group"); - - useSubscribeToInvalidationState([group.id], reload); - - const isMember = useIsGroupMember(group); - - return ( -
-

-
- - {group.slug} -
-

- -
- - - - - {isMember && } -
-
{children}
-
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/GroupModelList.tsx b/packages/hub/src/app/groups/[slug]/GroupModelList.tsx deleted file mode 100644 index 27fb1e43fa..0000000000 --- a/packages/hub/src/app/groups/[slug]/GroupModelList.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { ModelList } from "@/models/components/ModelList"; - -import { useIsGroupMember } from "./hooks"; - -import { GroupModelList$key } from "@/__generated__/GroupModelList.graphql"; - -const Fragment = graphql` - fragment GroupModelList on Group - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "GroupModelListPaginationQuery") { - ...hooks_useIsGroupMember - models(first: $count, after: $cursor) - @connection(key: "GroupModelList_models") { - edges { - __typename - } - ...ModelList - } - } -`; - -type Props = { - groupRef: GroupModelList$key; -}; - -export const GroupModelList: FC = ({ groupRef }) => { - const { data: group, loadNext } = usePaginationFragment(Fragment, groupRef); - - const { models } = group; - const isMember = useIsGroupMember(group); - - return ( -
- {models.edges.length ? ( - - ) : ( -
- {isMember - ? "This group doesn't have any models." - : "This group does not have any public models."} -
- )} -
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/GroupPage.tsx b/packages/hub/src/app/groups/[slug]/GroupPage.tsx deleted file mode 100644 index beb93eafdb..0000000000 --- a/packages/hub/src/app/groups/[slug]/GroupPage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { GroupModelList } from "./GroupModelList"; - -import { GroupPageQuery } from "@/__generated__/GroupPageQuery.graphql"; - -const Query = graphql` - query GroupPageQuery($slug: String!) { - result: group(slug: $slug) { - __typename - ... on Group { - id - slug - ...GroupModelList - } - } - } -`; - -export const GroupPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ result }] = usePageQuery(Query, query); - - const group = extractFromGraphqlErrorUnion(result, "Group"); - - return ; -}; diff --git a/packages/hub/src/app/groups/[slug]/InviteForMe.tsx b/packages/hub/src/app/groups/[slug]/InviteForMe.tsx deleted file mode 100644 index c3bdef5202..0000000000 --- a/packages/hub/src/app/groups/[slug]/InviteForMe.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { Card } from "@/components/ui/Card"; -import { MutationButton } from "@/components/ui/MutationButton"; - -import { InviteForMe$key } from "@/__generated__/InviteForMe.graphql"; -import { - GroupInviteReaction, - InviteForMeMutation, -} from "@/__generated__/InviteForMeMutation.graphql"; - -const Fragment = graphql` - fragment InviteForMe on Group { - id - inviteForMe { - id - role - } - } -`; - -const Mutation = graphql` - mutation InviteForMeMutation($input: MutationReactToGroupInviteInput!) { - result: reactToGroupInvite(input: $input) { - __typename - ... on BaseError { - message - } - ... on ReactToGroupInviteResult { - invite { - id - } - } - } - } -`; - -const InviteReactButton: FC<{ - inviteId: string; - groupId: string; - action: GroupInviteReaction; - title: string; - theme?: "default" | "primary"; -}> = ({ inviteId, groupId, action, title, theme }) => { - return ( - - mutation={Mutation} - variables={{ - input: { inviteId, action }, - }} - updater={(store) => { - // updating the invites connection is hard, let's just reload page data - store.get(groupId)?.invalidateRecord(); - }} - expectedTypename="ReactToGroupInviteResult" - title={title} - theme={theme} - > - ); -}; - -type Props = { - groupRef: InviteForMe$key; -}; - -export const InviteForMe: FC = ({ groupRef }) => { - const group = useFragment(Fragment, groupRef); - - if (!group.inviteForMe) { - return null; - } - - const inviteId = group.inviteForMe.id; - - return ( - -
-
{"You've been invited to this group."}
-
- - -
-
-
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/NewModelButton.tsx b/packages/hub/src/app/groups/[slug]/NewModelButton.tsx new file mode 100644 index 0000000000..e22e36f5fe --- /dev/null +++ b/packages/hub/src/app/groups/[slug]/NewModelButton.tsx @@ -0,0 +1,29 @@ +"use client"; +import { useRouter, useSelectedLayoutSegment } from "next/navigation"; +import { FC } from "react"; + +import { Button, PlusIcon } from "@quri/ui"; + +import { newModelRoute } from "@/lib/routes"; + +// TODO - this could be a server component, if we had `` component +export const NewModelButton: FC<{ group: string }> = ({ group }) => { + const segment = useSelectedLayoutSegment(); + + const link = newModelRoute({ group }); + + const router = useRouter(); + + if (segment === "members" || segment === "invite-link") { + return null; + } + + return ( + + ); +}; diff --git a/packages/hub/src/app/groups/[slug]/hooks.ts b/packages/hub/src/app/groups/[slug]/hooks.ts deleted file mode 100644 index dbcd099949..0000000000 --- a/packages/hub/src/app/groups/[slug]/hooks.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { graphql, useFragment } from "react-relay"; - -import { hooks_useIsGroupAdmin$key } from "@/__generated__/hooks_useIsGroupAdmin.graphql"; -import { hooks_useIsGroupMember$key } from "@/__generated__/hooks_useIsGroupMember.graphql"; - -export function useIsGroupAdmin(groupRef: hooks_useIsGroupAdmin$key) { - const { myMembership } = useFragment( - graphql` - fragment hooks_useIsGroupAdmin on Group { - myMembership { - id - role - } - } - `, - groupRef - ); - return myMembership?.role === "Admin"; -} - -export function useIsGroupMember(groupRef: hooks_useIsGroupMember$key) { - const { myMembership } = useFragment( - graphql` - fragment hooks_useIsGroupMember on Group { - myMembership { - id - } - } - `, - groupRef - ); - return Boolean(myMembership); -} diff --git a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx index 674e71345b..a6e2943709 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx @@ -1,137 +1,36 @@ "use client"; -import { useSession } from "next-auth/react"; -import { redirect, useRouter, useSearchParams } from "next/navigation"; -import { FC, useEffect } from "react"; -import { graphql } from "relay-runtime"; +import { useRouter } from "next/navigation"; +import { FC } from "react"; import { useToast } from "@quri/ui"; -import { MutationButton } from "@/components/ui/MutationButton"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { groupRoute } from "@/routes"; - -import { useIsGroupMember } from "../hooks"; - -import { AcceptGroupInvitePage_ValidateMutation } from "@/__generated__/AcceptGroupInvitePage_ValidateMutation.graphql"; -import { AcceptGroupInvitePageMutation } from "@/__generated__/AcceptGroupInvitePageMutation.graphql"; -import { AcceptGroupInvitePageQuery } from "@/__generated__/AcceptGroupInvitePageQuery.graphql"; +import { SafeActionButton } from "@/components/ui/SafeActionButton"; +import { acceptReusableGroupInviteTokenAction } from "@/groups/actions/acceptReusableGroupInviteTokenAction"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { groupRoute } from "@/lib/routes"; export const AcceptGroupInvitePage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - useSession({ required: true }); - - const [{ result }] = usePageQuery( - graphql` - query AcceptGroupInvitePageQuery($slug: String!) { - result: group(slug: $slug) { - __typename - ... on Group { - slug - ...hooks_useIsGroupMember - } - } - } - `, - query - ); - const group = extractFromGraphqlErrorUnion(result, "Group"); - const isGroupMember = useIsGroupMember(group); - - if (isGroupMember) { - redirect(groupRoute({ slug: group.slug })); - } - - const params = useSearchParams(); - const inviteToken = params.get("token"); - if (!inviteToken) { - throw new Error("Token is missing"); - } - - const [validateMutation] = useAsyncMutation< - AcceptGroupInvitePage_ValidateMutation, - "ValidateReusableGroupInviteTokenResult" - >({ - mutation: graphql` - mutation AcceptGroupInvitePage_ValidateMutation( - $input: MutationValidateReusableGroupInviteTokenInput! - ) { - result: validateReusableGroupInviteToken(input: $input) { - __typename - ... on BaseError { - message - } - ... on ValidateReusableGroupInviteTokenResult { - ok - } - } - } - `, - expectedTypename: "ValidateReusableGroupInviteTokenResult", - }); - + group: GroupCardDTO; + inviteToken: string; +}> = ({ group, inviteToken }) => { const toast = useToast(); const router = useRouter(); - useEffect(() => { - validateMutation({ - variables: { - input: { - groupSlug: group.slug, - inviteToken, - }, - }, - onCompleted({ ok }) { - if (!ok) { - toast("Invalid token", "error"); - router.replace(groupRoute({ slug: group.slug })); - } - }, - }); - }, []); - return (

{`You've been invited to join ${group.slug} group.`}

- - mutation={graphql` - mutation AcceptGroupInvitePageMutation( - $input: MutationAcceptReusableGroupInviteTokenInput! - ) { - result: acceptReusableGroupInviteToken(input: $input) { - __typename - ... on BaseError { - message - } - ... on AcceptReusableGroupInviteTokenResult { - __typename - membership { - group { - id - slug - ...hooks_useIsGroupMember - } - } - } - } - } - `} - expectedTypename="AcceptReusableGroupInviteTokenResult" - variables={{ - input: { - groupSlug: group.slug, - inviteToken, - }, - }} + toast("Joined", "confirmation")} + action={acceptReusableGroupInviteTokenAction} + onSuccess={() => { + toast("Joined", "confirmation"); + router.push(groupRoute({ slug: group.slug })); + }} + input={{ + groupSlug: group.slug, + inviteToken, + }} />
); diff --git a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx index dcb4ac3a3e..52992504c0 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx @@ -1,20 +1,61 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { notFound, redirect } from "next/navigation"; +import { z } from "zod"; -import { AcceptGroupInvitePage } from "./AcceptGroupInvitePage"; +import { WithAuth } from "@/components/WithAuth"; +import { loadGroupCard } from "@/groups/data/groupCards"; +import { + hasGroupMembership, + validateReusableGroupInviteToken, +} from "@/groups/data/helpers"; +import { groupRoute } from "@/lib/routes"; -import QueryNode, { - AcceptGroupInvitePageQuery, -} from "@/__generated__/AcceptGroupInvitePageQuery.graphql"; +import { AcceptGroupInvitePage } from "./AcceptGroupInvitePage"; type Props = { params: Promise<{ slug: string }>; + searchParams: Promise<{ token: string }>; }; -export default async function OuterAcceptGroupInvitePage({ params }: Props) { +async function InnerPage({ params, searchParams }: Props) { const { slug } = await params; - const query = await loadPageQuery(QueryNode, { - slug, + + const { token: inviteToken } = z + .object({ + token: z.string(), + }) + .parse(await searchParams); + + const group = await loadGroupCard(slug); + if (!group) { + notFound(); + } + const isMember = await hasGroupMembership(slug); + if (isMember) { + redirect(groupRoute({ slug: group.slug })); + } + + const isValidToken = await validateReusableGroupInviteToken({ + groupSlug: group.slug, + inviteToken, }); - return ; + if (!isValidToken) { + // FIXME - copy-pasted from ErrorBoudnary + return ( +
+
Error
+
Invalid invite token.
+
+ ); + } + + return ; +} + +export default async function (props: Props) { + return ( + + + + ); } diff --git a/packages/hub/src/app/groups/[slug]/layout.tsx b/packages/hub/src/app/groups/[slug]/layout.tsx index a08fef4a4f..1d01d465f1 100644 --- a/packages/hub/src/app/groups/[slug]/layout.tsx +++ b/packages/hub/src/app/groups/[slug]/layout.tsx @@ -1,28 +1,58 @@ import { Metadata } from "next"; +import { notFound } from "next/navigation"; import { PropsWithChildren } from "react"; -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { GroupIcon } from "@quri/ui"; -import { GroupLayout } from "./GroupLayout"; +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { H1 } from "@/components/ui/Headers"; +import { + StyledTabLink, + StyledTabLinkList, +} from "@/components/ui/StyledTabLink"; +import { loadGroupCard } from "@/groups/data/groupCards"; +import { hasGroupMembership } from "@/groups/data/helpers"; +import { groupMembersRoute, groupRoute } from "@/lib/routes"; -import QueryNode, { - GroupLayoutQuery, -} from "@/__generated__/GroupLayoutQuery.graphql"; +import { NewModelButton } from "./NewModelButton"; type Props = PropsWithChildren<{ params: Promise<{ slug: string }>; }>; -export default async function OuterGroupLayout({ params, children }: Props) { +export default async function GroupLayout({ params, children }: Props) { const { slug } = await params; - const query = await loadPageQuery(QueryNode, { - slug, - }); + const group = await loadGroupCard(slug); + if (!group) { + notFound(); + } + + const isMember = await hasGroupMembership(slug); return ( - {children} +
+

+
+ + {group.slug} +
+

+
+ + + + + {isMember && } +
+
{children}
+
); } diff --git a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx index 42ac97c2a6..c218c1842b 100644 --- a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx @@ -1,80 +1,37 @@ +import { MembershipRole } from "@prisma/client"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { ConnectionHandler, graphql } from "relay-runtime"; import { PlusIcon, SelectStringFormField } from "@quri/ui"; import { SelectUser, SelectUserOption } from "@/components/SelectUser"; -import { MutationModalAction } from "@/components/ui/MutationModalAction"; - -import { AddUserToGroupAction_group$key } from "@/__generated__/AddUserToGroupAction_group.graphql"; -import { AddUserToGroupActionMutation } from "@/__generated__/AddUserToGroupActionMutation.graphql"; -import { MembershipRole } from "@/__generated__/SetMembershipRoleActionMutation.graphql"; - -const Mutation = graphql` - mutation AddUserToGroupActionMutation( - $input: MutationAddUserToGroupInput! - $connections: [ID!]! - ) { - result: addUserToGroup(input: $input) { - __typename - ... on BaseError { - message - } - ... on AddUserToGroupResult { - membership - @appendNode( - connections: $connections - edgeTypeName: "UserGroupMembershipEdge" - ) { - ...GroupMemberCard - } - } - } - } -`; +import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; +import { addUserToGroupAction } from "@/groups/actions/addUserToGroupAction"; +import { GroupMemberDTO } from "@/groups/data/members"; type Props = { - groupRef: AddUserToGroupAction_group$key; - close: () => void; + groupSlug: string; + append: (item: GroupMemberDTO) => void; }; type FormShape = { user: SelectUserOption; role: MembershipRole }; -export const AddUserToGroupAction: FC = ({ groupRef, close }) => { - const group = useFragment( - graphql` - fragment AddUserToGroupAction_group on Group { - id - slug - } - `, - groupRef - ); - +export const AddUserToGroupAction: FC = ({ groupSlug, append }) => { return ( - + title="Add" icon={PlusIcon} - close={close} - mutation={Mutation} - expectedTypename="AddUserToGroupResult" + action={addUserToGroupAction} + onSuccess={(membership) => { + append(membership); + }} defaultValues={{ role: "Member" }} - formDataToVariables={(data) => ({ - input: { - group: group.slug, - username: data.user.username, - role: data.role, - }, - connections: [ - ConnectionHandler.getConnectionID( - group.id, - "GroupMemberList_memberships" - ), - ], + formDataToInput={(data) => ({ + group: groupSlug, + username: data.user.slug, + role: data.role, })} submitText="Add" - modalTitle={`Add to group ${group.slug}`} + modalTitle={`Add to group ${groupSlug}`} > {() => (
@@ -87,6 +44,6 @@ export const AddUserToGroupAction: FC = ({ groupRef, close }) => { />
)} - + ); }; diff --git a/packages/hub/src/app/groups/[slug]/members/CancelInviteAction.tsx b/packages/hub/src/app/groups/[slug]/members/CancelInviteAction.tsx deleted file mode 100644 index f88bddab76..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/CancelInviteAction.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC } from "react"; -import { ConnectionHandler, graphql } from "relay-runtime"; - -import { TrashIcon } from "@quri/ui"; - -import { MutationAction } from "@/components/ui/MutationAction"; - -import { CancelInviteActionMutation } from "@/__generated__/CancelInviteActionMutation.graphql"; - -type Props = { - inviteId: string; - groupId: string; - close: () => void; -}; - -export const CancelInviteAction: FC = ({ inviteId, groupId, close }) => { - return ( - - title="Cancel" - icon={TrashIcon} - mutation={graphql` - mutation CancelInviteActionMutation( - $input: MutationCancelGroupInviteInput! - $connections: [ID!]! - ) { - result: cancelGroupInvite(input: $input) { - __typename - ... on BaseError { - message - } - ... on CancelGroupInviteResult { - invite { - id @deleteEdge(connections: $connections) - } - } - } - } - `} - expectedTypename="CancelGroupInviteResult" - variables={{ - input: { inviteId }, - connections: [ - ConnectionHandler.getConnectionID(groupId, "GroupInviteList_invites"), - ], - }} - close={close} - /> - ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx b/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx index aa57674192..0abcd5e623 100644 --- a/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx @@ -1,93 +1,32 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { ConnectionHandler, graphql } from "relay-runtime"; -import { DropdownMenuAsyncActionItem, TrashIcon } from "@quri/ui"; +import { TrashIcon } from "@quri/ui"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; - -import { DeleteMembershipAction_Group$key } from "@/__generated__/DeleteMembershipAction_Group.graphql"; -import { DeleteMembershipAction_Membership$key } from "@/__generated__/DeleteMembershipAction_Membership.graphql"; -import { DeleteMembershipActionMutation } from "@/__generated__/DeleteMembershipActionMutation.graphql"; - -const Mutation = graphql` - mutation DeleteMembershipActionMutation( - $input: MutationDeleteMembershipInput! - ) { - result: deleteMembership(input: $input) { - __typename - ... on BaseError { - message - } - ... on DeleteMembershipResult { - ok - } - } - } -`; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; +import { deleteMembershipAction } from "@/groups/actions/deleteMembershipAction"; +import { GroupMemberDTO } from "@/groups/data/members"; type Props = { - groupRef: DeleteMembershipAction_Group$key; - membershipRef: DeleteMembershipAction_Membership$key; - close: () => void; + groupSlug: string; + membership: GroupMemberDTO; + remove: (item: GroupMemberDTO) => void; }; export const DeleteMembershipAction: FC = ({ - groupRef, - membershipRef, - close, + groupSlug, + membership, + remove, }) => { - const [runMutation] = useAsyncMutation({ - mutation: Mutation, - expectedTypename: "DeleteMembershipResult", - }); - - const group = useFragment( - graphql` - fragment DeleteMembershipAction_Group on Group { - id - slug - } - `, - groupRef - ); - - const membership = useFragment( - graphql` - fragment DeleteMembershipAction_Membership on UserGroupMembership { - id - user { - slug - } - } - `, - membershipRef - ); - - const act = async () => { - await runMutation({ - variables: { - input: { group: group.slug, user: membership.user.slug }, - }, - updater: (store) => { - const groupRecord = store.get(group.id); - if (!groupRecord) return; - const connectionRecord = ConnectionHandler.getConnection( - groupRecord, - "GroupMemberList_memberships" - ); - if (!connectionRecord) return; - ConnectionHandler.deleteNode(connectionRecord, membership.id); - }, - }); - }; - return ( - remove(membership)} /> ); }; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupInviteCard.tsx b/packages/hub/src/app/groups/[slug]/members/GroupInviteCard.tsx deleted file mode 100644 index 797abefcd4..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/GroupInviteCard.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; - -import { DropdownMenu } from "@quri/ui"; - -import { Card } from "@/components/ui/Card"; -import { DotsDropdown } from "@/components/ui/DotsDropdown"; - -import { CancelInviteAction } from "./CancelInviteAction"; -import { InviteRoleButton } from "./InviteRoleButton"; -import { UserGroupInvite } from "./UserGroupInvite"; - -import { GroupInviteCard$key } from "@/__generated__/GroupInviteCard.graphql"; - -export const GroupInviteCard: FC<{ - inviteRef: GroupInviteCard$key; - groupId: string; -}> = (props) => { - const invite = useFragment( - graphql` - fragment GroupInviteCard on GroupInvite { - __typename - id - role - ...InviteRoleButton - ...UserGroupInvite - } - `, - props.inviteRef - ); - - return ( - -
-
- {invite.__typename === "UserGroupInvite" ? ( - - ) : ( - "Unknown invite type" - )} -
-
- Invited as: - - - {({ close }) => ( - - - - )} - -
-
-
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupInviteList.tsx b/packages/hub/src/app/groups/[slug]/members/GroupInviteList.tsx deleted file mode 100644 index 3ea33884d5..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/GroupInviteList.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { LoadMore } from "@/components/LoadMore"; -import { H2 } from "@/components/ui/Headers"; - -import { GroupInviteCard } from "./GroupInviteCard"; - -import { GroupInviteList$key } from "@/__generated__/GroupInviteList.graphql"; -import { GroupInviteListPaginationQuery } from "@/__generated__/GroupInviteListPaginationQuery.graphql"; - -const fragment = graphql` - fragment GroupInviteList on Group - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "GroupInviteListPaginationQuery") { - invites(first: $count, after: $cursor) - @connection(key: "GroupInviteList_invites") { - edges { - node { - id - ...GroupInviteCard - } - } - pageInfo { - hasNextPage - } - } - } -`; - -type Props = { - groupRef: GroupInviteList$key; -}; - -export const GroupInviteList: FC = ({ groupRef }) => { - const { data: group, loadNext } = usePaginationFragment< - GroupInviteListPaginationQuery, - GroupInviteList$key - >(fragment, groupRef); - - return group.invites?.edges.length ? ( -
-

Pending invites

-
- {group.invites.edges.map(({ node: invite }) => ( - - ))} -
- {group.invites.pageInfo.hasNextPage && } -
- ) : null; -}; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx index 6ac649c61b..6087034438 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx @@ -1,75 +1,44 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { DropdownMenu } from "@quri/ui"; import { Card } from "@/components/ui/Card"; import { DotsDropdown } from "@/components/ui/DotsDropdown"; import { StyledLink } from "@/components/ui/StyledLink"; -import { userRoute } from "@/routes"; +import { GroupMemberDTO } from "@/groups/data/members"; +import { userRoute } from "@/lib/routes"; -import { useIsGroupAdmin } from "../hooks"; import { DeleteMembershipAction } from "./DeleteMembershipAction"; import { MembershipRoleButton } from "./MembershipRoleButton"; -import { GroupMemberCard$key } from "@/__generated__/GroupMemberCard.graphql"; -import { GroupMemberCard_group$key } from "@/__generated__/GroupMemberCard_group.graphql"; - export const GroupMemberCard: FC<{ - membershipRef: GroupMemberCard$key; - groupRef: GroupMemberCard_group$key; -}> = ({ membershipRef, groupRef }) => { - const membership = useFragment( - graphql` - fragment GroupMemberCard on UserGroupMembership { - id - role - user { - id - username - } - ...DeleteMembershipAction_Membership - ...MembershipRoleButton_Membership - } - `, - membershipRef - ); - - const group = useFragment( - graphql` - fragment GroupMemberCard_group on Group { - id - ...hooks_useIsGroupAdmin - ...DeleteMembershipAction_Group - ...MembershipRoleButton_Group - } - `, - groupRef - ); - - const isAdmin = useIsGroupAdmin(group); - + groupSlug: string; + isAdmin: boolean; + membership: GroupMemberDTO; + remove: (membership: GroupMemberDTO) => void; + update: (membership: GroupMemberDTO) => void; +}> = ({ groupSlug, isAdmin, membership, remove, update }) => { return (
- - {membership.user.username} + + {membership.user.slug}
{isAdmin ? (
- {({ close }) => ( + {() => ( )} diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx index 0e7aa9db1e..6614b83309 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx @@ -1,57 +1,44 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; +"use client"; +import { FC, useCallback } from "react"; import { DropdownMenu } from "@quri/ui"; import { LoadMore } from "@/components/LoadMore"; import { DotsDropdown } from "@/components/ui/DotsDropdown"; import { H2 } from "@/components/ui/Headers"; +import { GroupMemberDTO } from "@/groups/data/members"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; -import { useIsGroupAdmin } from "../hooks"; import { AddUserToGroupAction } from "./AddUserToGroupAction"; import { GroupMemberCard } from "./GroupMemberCard"; -import { GroupMemberList$key } from "@/__generated__/GroupMemberList.graphql"; -import { GroupMemberListPaginationQuery } from "@/__generated__/GroupMemberListPaginationQuery.graphql"; - -const fragment = graphql` - fragment GroupMemberList on Group - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "GroupMemberListPaginationQuery") { - ...hooks_useIsGroupAdmin - ...AddUserToGroupAction_group - ...GroupMemberCard_group - - memberships(first: $count, after: $cursor) - @connection(key: "GroupMemberList_memberships") { - edges { - node { - id - ...GroupMemberCard - } - } - pageInfo { - hasNextPage - } - } - } -`; - type Props = { - groupRef: GroupMemberList$key; + groupSlug: string; + page: Paginated; + isAdmin: boolean; }; -export const GroupMemberList: FC = ({ groupRef }) => { - const { data: group, loadNext } = usePaginationFragment< - GroupMemberListPaginationQuery, - GroupMemberList$key - >(fragment, groupRef); +export const GroupMemberList: FC = ({ + groupSlug, + page: initialPage, + isAdmin, +}) => { + const page = usePaginator(initialPage); - const isAdmin = useIsGroupAdmin(group); + const updateMembership = useCallback( + (membership: GroupMemberDTO) => { + page.update((item) => (item.id === membership.id ? membership : item)); + }, + [page] + ); + + const removeMembership = useCallback( + (membership: GroupMemberDTO) => { + page.remove((item) => item.id === membership.id); + }, + [page] + ); return (
@@ -59,26 +46,30 @@ export const GroupMemberList: FC = ({ groupRef }) => {

Members

{isAdmin && ( - {({ close }) => ( + {() => ( - + )} )}
- {group.memberships.edges.map(({ node: membership }) => ( + {page.items.map((membership) => ( ))}
- {group.memberships.pageInfo.hasNextPage && ( - - )} + {page.loadNext && }
); }; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMembersPage.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMembersPage.tsx deleted file mode 100644 index 705d276314..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/GroupMembersPage.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; -import { FC } from "react"; -import { useSubscribeToInvalidationState } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { useIsGroupAdmin } from "../hooks"; -import { GroupInviteList } from "./GroupInviteList"; -import { GroupMemberList } from "./GroupMemberList"; -import { GroupReusableInviteSection } from "./GroupReusableInviteSection"; - -import { GroupMembersPageQuery } from "@/__generated__/GroupMembersPageQuery.graphql"; - -const Query = graphql` - query GroupMembersPageQuery($slug: String!) { - result: group(slug: $slug) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on Group { - id - ...GroupMemberList - ...GroupInviteList - ...GroupReusableInviteSection - ...hooks_useIsGroupAdmin - } - } - } -`; - -export const GroupMembersPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ result }, { reload }] = usePageQuery(Query, query); - - const group = extractFromGraphqlErrorUnion(result, "Group"); - - useSubscribeToInvalidationState([group.id], reload); - - const isAdmin = useIsGroupAdmin(group); - - return ( -
-
- -
- {isAdmin && ( - <> -
- -
-
- -
- - )} -
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx b/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx index 3bcc3f1b52..b5b516fc98 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx @@ -1,34 +1,24 @@ +"use client"; import { FC, useEffect, useMemo, useState } from "react"; import Skeleton from "react-loading-skeleton"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { ClipboardCopyIcon, TextTooltip, useToast } from "@quri/ui"; import { H2 } from "@/components/ui/Headers"; -import { MutationButton } from "@/components/ui/MutationButton"; -import { groupInviteLink } from "@/routes"; - -import { GroupReusableInviteSection$key } from "@/__generated__/GroupReusableInviteSection.graphql"; -import { GroupReusableInviteSection_CreateMutation } from "@/__generated__/GroupReusableInviteSection_CreateMutation.graphql"; -import { GroupReusableInviteSection_DeleteMutation } from "@/__generated__/GroupReusableInviteSection_DeleteMutation.graphql"; +import { SafeActionButton } from "@/components/ui/SafeActionButton"; +import { createReusableGroupInviteTokenAction } from "@/groups/actions/createReusableGroupInviteTokenAction"; +import { deleteReusableGroupInviteTokenAction } from "@/groups/actions/deleteReusableGroupInviteTokenAction"; +import { groupInviteLink } from "@/lib/routes"; type Props = { - groupRef: GroupReusableInviteSection$key; + groupSlug: string; + reusableInviteToken: string | null; }; -export const GroupReusableInviteSection: FC = ({ groupRef }) => { - const group = useFragment( - graphql` - fragment GroupReusableInviteSection on Group { - id - slug - reusableInviteToken - } - `, - groupRef - ); - +export const GroupReusableInviteSection: FC = ({ + groupSlug, + reusableInviteToken, +}) => { const toast = useToast(); // Necessary for SSR and to avoid hydration errors @@ -36,12 +26,12 @@ export const GroupReusableInviteSection: FC = ({ groupRef }) => { useEffect(() => setOrigin(window.location.origin), []); const inviteLink = useMemo(() => { - if (!group.reusableInviteToken) { + if (!reusableInviteToken) { return undefined; } const routeArgs = { - groupSlug: group.slug, - inviteToken: group.reusableInviteToken, + groupSlug: groupSlug, + inviteToken: reusableInviteToken, }; const fullLink = groupInviteLink(routeArgs); const blurredLink = groupInviteLink({ ...routeArgs, blur: true }); @@ -50,7 +40,7 @@ export const GroupReusableInviteSection: FC = ({ groupRef }) => { full: `${origin}${fullLink}`, blurred: `${origin}${blurredLink}`, }; - }, [group.reusableInviteToken, group.slug, origin]); + }, [reusableInviteToken, groupSlug, origin]); const copy = () => { if (!inviteLink) { @@ -82,63 +72,17 @@ export const GroupReusableInviteSection: FC = ({ groupRef }) => { ) ) : null}
- - mutation={graphql` - mutation GroupReusableInviteSection_CreateMutation( - $input: MutationCreateReusableGroupInviteTokenInput! - ) { - result: createReusableGroupInviteToken(input: $input) { - __typename - ... on BaseError { - message - } - ... on CreateReusableGroupInviteTokenResult { - group { - reusableInviteToken - } - } - } - } - `} - expectedTypename="CreateReusableGroupInviteTokenResult" - variables={{ - input: { slug: group.slug }, - }} + - {group.reusableInviteToken ? ( - - mutation={graphql` - mutation GroupReusableInviteSection_DeleteMutation( - $input: MutationDeleteReusableGroupInviteTokenInput! - ) { - result: deleteReusableGroupInviteToken(input: $input) { - __typename - ... on BaseError { - message - } - ... on DeleteReusableGroupInviteTokenResult { - group { - reusableInviteToken - } - } - } - } - `} - expectedTypename="DeleteReusableGroupInviteTokenResult" - variables={{ - input: { slug: group.slug }, - }} + {reusableInviteToken ? ( + ) : null} diff --git a/packages/hub/src/app/groups/[slug]/members/InviteRoleButton.tsx b/packages/hub/src/app/groups/[slug]/members/InviteRoleButton.tsx deleted file mode 100644 index 2917de36d0..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/InviteRoleButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { Button, Dropdown, DropdownMenu } from "@quri/ui"; - -import { SetInviteRoleButton } from "./SetInviteRoleAction"; - -import { - InviteRoleButton$key, - MembershipRole, -} from "@/__generated__/InviteRoleButton.graphql"; - -const Fragment = graphql` - fragment InviteRoleButton on GroupInvite { - id - role - } -`; - -type Props = { - inviteRef: InviteRoleButton$key; -}; - -export const InviteRoleButton: FC = ({ inviteRef }) => { - const invite = useFragment(Fragment, inviteRef); - - return ( - ( - - {(["Admin", "Member"] satisfies MembershipRole[]).map((role) => ( - - ))} - - )} - > - - - ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx b/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx index a402ab9fcd..0ea099986f 100644 --- a/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx +++ b/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx @@ -1,57 +1,34 @@ +import { type MembershipRole } from "@prisma/client"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { Button, Dropdown, DropdownMenu } from "@quri/ui"; -import { SetMembershipRoleAction } from "./SetMembershipRoleAction"; +import { GroupMemberDTO } from "@/groups/data/members"; -import { MembershipRoleButton_Group$key } from "@/__generated__/MembershipRoleButton_Group.graphql"; -import { - MembershipRole, - MembershipRoleButton_Membership$key, -} from "@/__generated__/MembershipRoleButton_Membership.graphql"; +import { SetMembershipRoleAction } from "./SetMembershipRoleAction"; type Props = { - membershipRef: MembershipRoleButton_Membership$key; - groupRef: MembershipRoleButton_Group$key; + groupSlug: string; + membership: GroupMemberDTO; + update: (membership: GroupMemberDTO) => void; }; export const MembershipRoleButton: FC = ({ - membershipRef, - groupRef, + membership, + groupSlug, + update, }) => { - const group = useFragment( - graphql` - fragment MembershipRoleButton_Group on Group { - ...SetMembershipRoleAction_Group - } - `, - groupRef - ); - - const membership = useFragment( - graphql` - fragment MembershipRoleButton_Membership on UserGroupMembership { - id - role - ...SetMembershipRoleAction_Membership - } - `, - membershipRef - ); - return ( ( + render={() => ( {(["Admin", "Member"] satisfies MembershipRole[]).map((role) => ( ))} diff --git a/packages/hub/src/app/groups/[slug]/members/SetInviteRoleAction.tsx b/packages/hub/src/app/groups/[slug]/members/SetInviteRoleAction.tsx deleted file mode 100644 index 01728c673a..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/SetInviteRoleAction.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { MutationAction } from "@/components/ui/MutationAction"; - -import { SetInviteRoleActionMutation } from "@/__generated__/SetInviteRoleActionMutation.graphql"; -import { MembershipRole } from "@/__generated__/SetMembershipRoleActionMutation.graphql"; - -const Mutation = graphql` - mutation SetInviteRoleActionMutation( - $input: MutationUpdateGroupInviteRoleInput! - ) { - result: updateGroupInviteRole(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateGroupInviteRoleResult { - invite { - id - role - } - } - } - } -`; - -type Props = { - inviteId: string; - role: MembershipRole; - close: () => void; -}; - -export const SetInviteRoleButton: FC = ({ inviteId, role, close }) => { - return ( - - mutation={Mutation} - variables={{ - input: { - inviteId, - role, - }, - }} - expectedTypename="UpdateGroupInviteRoleResult" - title={role} - close={close} - /> - ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx index 92a676f21e..518463021c 100644 --- a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx @@ -1,86 +1,35 @@ +import { MembershipRole } from "@prisma/client"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; -import { MutationAction } from "@/components/ui/MutationAction"; - -import { SetMembershipRoleAction_Group$key } from "@/__generated__/SetMembershipRoleAction_Group.graphql"; -import { SetMembershipRoleAction_Membership$key } from "@/__generated__/SetMembershipRoleAction_Membership.graphql"; -import { - MembershipRole, - SetMembershipRoleActionMutation, -} from "@/__generated__/SetMembershipRoleActionMutation.graphql"; - -const Mutation = graphql` - mutation SetMembershipRoleActionMutation( - $input: MutationUpdateMembershipRoleInput! - ) { - result: updateMembershipRole(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateMembershipRoleResult { - membership { - id - role - } - } - } - } -`; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; +import { updateMembershipRoleAction } from "@/groups/actions/updateMembershipRoleAction"; +import { GroupMemberDTO } from "@/groups/data/members"; type Props = { - groupRef: SetMembershipRoleAction_Group$key; - membershipRef: SetMembershipRoleAction_Membership$key; + membership: GroupMemberDTO; + groupSlug: string; role: MembershipRole; - close: () => void; + update: (membership: GroupMemberDTO) => void; }; export const SetMembershipRoleAction: FC = ({ - groupRef, - membershipRef, + membership, + groupSlug, role, - close, + update, }) => { - const group = useFragment( - graphql` - fragment SetMembershipRoleAction_Group on Group { - id - slug - } - `, - groupRef - ); - - const membership = useFragment( - graphql` - fragment SetMembershipRoleAction_Membership on UserGroupMembership { - id - user { - slug - } - } - `, - membershipRef - ); - return ( - - mutation={Mutation} - variables={{ - input: { - user: membership.user.slug, - group: group.slug, - role, - }, + { + update(newMembership); }} - expectedTypename="UpdateMembershipRoleResult" title={role} - close={close} /> ); }; diff --git a/packages/hub/src/app/groups/[slug]/members/UserGroupInvite.tsx b/packages/hub/src/app/groups/[slug]/members/UserGroupInvite.tsx deleted file mode 100644 index abfae56b53..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/UserGroupInvite.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { StyledLink } from "@/components/ui/StyledLink"; -import { userRoute } from "@/routes"; - -import { UserGroupInvite$key } from "@/__generated__/UserGroupInvite.graphql"; - -export const UserGroupInvite: FC<{ inviteRef: UserGroupInvite$key }> = ({ - inviteRef, -}) => { - const invite = useFragment( - graphql` - fragment UserGroupInvite on UserGroupInvite { - id - user { - username - } - } - `, - inviteRef - ); - - return ( - - {invite.user.username} - - ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/page.tsx b/packages/hub/src/app/groups/[slug]/members/page.tsx index cb180889c2..ce645624fc 100644 --- a/packages/hub/src/app/groups/[slug]/members/page.tsx +++ b/packages/hub/src/app/groups/[slug]/members/page.tsx @@ -1,10 +1,11 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { + loadGroupMembers, + loadMyMembership, + loadReusableInviteToken, +} from "@/groups/data/members"; -import { GroupMembersPage } from "./GroupMembersPage"; - -import QueryNode, { - GroupMembersPageQuery, -} from "@/__generated__/GroupMembersPageQuery.graphql"; +import { GroupMemberList } from "./GroupMemberList"; +import { GroupReusableInviteSection } from "./GroupReusableInviteSection"; type Props = { params: Promise<{ slug: string }>; @@ -12,9 +13,26 @@ type Props = { export default async function OuterGroupMembersPage({ params }: Props) { const { slug } = await params; - const query = await loadPageQuery(QueryNode, { - slug, - }); + const members = await loadGroupMembers({ groupSlug: slug }); + + const myMembership = await loadMyMembership({ groupSlug: slug }); + const isAdmin = myMembership?.role === "Admin"; - return ; + return ( +
+
+ +
+ {isAdmin && ( +
+ +
+ )} +
+ ); } diff --git a/packages/hub/src/app/groups/[slug]/page.tsx b/packages/hub/src/app/groups/[slug]/page.tsx index abfe56f6c1..a40bc7acce 100644 --- a/packages/hub/src/app/groups/[slug]/page.tsx +++ b/packages/hub/src/app/groups/[slug]/page.tsx @@ -1,10 +1,6 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { GroupPage } from "./GroupPage"; - -import QueryNode, { - GroupPageQuery, -} from "@/__generated__/GroupPageQuery.graphql"; +import { hasGroupMembership } from "@/groups/data/helpers"; +import { ModelList } from "@/models/components/ModelList"; +import { loadModelCards } from "@/models/data/cards"; type Props = { params: Promise<{ slug: string }>; @@ -12,9 +8,23 @@ type Props = { export default async function OuterGroupPage({ params }: Props) { const { slug } = await params; - const query = await loadPageQuery(QueryNode, { - slug, + + const page = await loadModelCards({ + ownerSlug: slug, }); + const isMember = await hasGroupMembership(slug); - return ; + return ( +
+ {page.items.length ? ( + + ) : ( +
+ {isMember + ? "This group doesn't have any models." + : "This group does not have any public models."} +
+ )} +
+ ); } diff --git a/packages/hub/src/app/layout.tsx b/packages/hub/src/app/layout.tsx index aa416e4fab..20b0ad42fd 100644 --- a/packages/hub/src/app/layout.tsx +++ b/packages/hub/src/app/layout.tsx @@ -6,19 +6,15 @@ import { Analytics } from "@vercel/analytics/react"; import { Metadata } from "next"; import { PropsWithChildren } from "react"; -import { auth } from "@/auth"; - -import { RootLayout } from "./RootLayout"; +import { RootLayout } from "../components/layout/RootLayout"; export default async function ServerRootLayout({ children, }: PropsWithChildren) { - const session = await auth(); - return ( - {children} + {children} diff --git a/packages/hub/src/app/loading.tsx b/packages/hub/src/app/loading.tsx deleted file mode 100644 index 8f9c8feb4e..0000000000 --- a/packages/hub/src/app/loading.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Skeleton from "react-loading-skeleton"; - -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; - -export default function Loading() { - return ( - - - - ); -} diff --git a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx index b20eb7730c..cdeeb3738c 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx @@ -1,79 +1,54 @@ +import { useAction } from "next-safe-action/hooks"; import { useRouter } from "next/navigation"; -import { FC, useCallback } from "react"; -import { useFragment, useMutation } from "react-relay"; -import { graphql } from "relay-runtime"; +import { FC } from "react"; -import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; +import { + DropdownMenuActionItem, + TrashIcon, + useCloseDropdown, + useToast, +} from "@quri/ui"; -import { ownerRoute } from "@/routes"; - -import { DeleteModelAction$key } from "@/__generated__/DeleteModelAction.graphql"; -import { DeleteModelActionMutation } from "@/__generated__/DeleteModelActionMutation.graphql"; - -const Mutation = graphql` - mutation DeleteModelActionMutation($input: MutationDeleteModelInput!) { - deleteModel(input: $input) { - __typename - ... on BaseError { - message - } - } - } -`; +import { ownerRoute } from "@/lib/routes"; +import { deleteModelAction } from "@/models/actions/deleteModelAction"; +import { ModelCardDTO } from "@/models/data/cards"; type Props = { - model: DeleteModelAction$key; - close(): void; + model: ModelCardDTO; }; -export const DeleteModelAction: FC = ({ model: modelKey, close }) => { - const model = useFragment( - graphql` - fragment DeleteModelAction on Model { - slug - owner { - __typename - id - slug - } - } - `, - modelKey - ); - +export const DeleteModelAction: FC = ({ model }) => { const router = useRouter(); - const [mutation] = useMutation(Mutation); - const toast = useToast(); - const onClick = useCallback((): Promise => { - return new Promise((resolve) => { - mutation({ - variables: { input: { owner: model.owner.slug, slug: model.slug } }, - onCompleted(response) { - if (response.deleteModel.__typename === "BaseError") { - toast(response.deleteModel.message, "error"); - resolve(); - } else { - // TODO - this is risky, what if we add more error types to GraphQL schema? - router.push(ownerRoute(model.owner)); - } - }, - onError(e) { - toast(e.toString(), "error"); - resolve(); - }, - }); - }); - }, [mutation, model.owner, model.slug, router, toast]); + const closeDropdown = useCloseDropdown(); + + const { execute, isPending } = useAction(deleteModelAction, { + onSuccess: ({ data }) => { + if (data) { + router.push(ownerRoute(model.owner)); + } + // we're going to redirect, so no need to close the dropdown + // TODO - keep the action in "acting" state while redirecting + }, + onError: ({ error }) => { + toast(error.serverError ?? "Internal error", "error"); + closeDropdown(); + }, + }); return ( - { + execute({ + owner: model.owner.slug, + slug: model.slug, + }); + }} + acting={isPending} icon={TrashIcon} - close={close} /> ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditModelPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditModelPage.tsx deleted file mode 100644 index d6155c74e7..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/EditModelPage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql } from "react-relay"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { EditSquiggleSnippetModel } from "./EditSquiggleSnippetModel"; - -import { EditModelPageQuery } from "@/__generated__/EditModelPageQuery.graphql"; - -export const EditModelPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ model: result }] = usePageQuery( - graphql` - query EditModelPageQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on BaseError { - message - } - ... on Model { - id - currentRevision { - content { - __typename - } - } - ...EditSquiggleSnippetModel - } - } - } - `, - query - ); - - const model = extractFromGraphqlErrorUnion(result, "Model"); - const typename = model.currentRevision.content.__typename; - - switch (typename) { - case "SquiggleSnippet": - return ; - default: - return
Unknown model type {typename}
; - } -}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index a150abde8f..6df4baed92 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -1,4 +1,4 @@ -import { useSession } from "next-auth/react"; +"use client"; import { useRouter } from "next/navigation"; import { BaseSyntheticEvent, @@ -9,7 +9,6 @@ import { useState, } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; -import { graphql, useFragment } from "react-relay"; import { ButtonWithDropdown, @@ -21,6 +20,7 @@ import { LinkIcon, TextAreaFormField, TextTooltip, + useToast, } from "@quri/ui"; import { checkSquiggleVersion, @@ -30,7 +30,6 @@ import { useAdjustSquiggleVersion, versionedSquigglePackages, versionSupportsDropdownMenu, - versionSupportsExports, versionSupportsImportTooltip, versionSupportsOnOpenExport, } from "@quri/versioned-squiggle-components"; @@ -39,17 +38,18 @@ import { useExitConfirmation } from "@/components/ExitConfirmationWrapper/hooks" import { EditRelativeValueExports } from "@/components/exports/EditRelativeValueExports"; import { ReactRoot } from "@/components/ReactRoot"; import { FormModal } from "@/components/ui/FormModal"; -import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/constants"; -import { useAvailableHeight } from "@/hooks/useAvailableHeight"; -import { useMutationForm } from "@/hooks/useMutationForm"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { modelRoute, variableRoute } from "@/routes"; +import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/lib/constants"; +import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; +import { modelRoute, variableRoute } from "@/lib/routes"; +import { updateSquiggleSnippetModelAction } from "@/models/actions/updateSquiggleSnippetModelAction"; +import { ModelFullDTO } from "@/models/data/full"; import { ImportTooltip } from "@/squiggle/components/ImportTooltip"; import { getHubLinker, parseSourceId, serializeSourceId, -} from "@/squiggle/components/linker"; +} from "@/squiggle/linker"; import { Draft, @@ -58,17 +58,20 @@ import { useDraftLocator, } from "./SquiggleSnippetDraftDialog"; -import { EditSquiggleSnippetModel$key } from "@/__generated__/EditSquiggleSnippetModel.graphql"; -import { - EditSquiggleSnippetModelMutation, - RelativeValuesExportInput, -} from "@/__generated__/EditSquiggleSnippetModelMutation.graphql"; - export type SquiggleSnippetFormShape = { code: string; - relativeValuesExports: RelativeValuesExportInput[]; + relativeValuesExports: { + variableName: string; + definition: { + owner: string; + slug: string; + }; + }[]; }; +export type RelativeValuesExportInput = + SquiggleSnippetFormShape["relativeValuesExports"][number]; + type OnSubmit = ( event?: BaseSyntheticEvent, extraData?: { comment: string } @@ -94,6 +97,7 @@ const SaveDialog: FC<{ onSubmit: OnSubmit; close: () => void }> = ({ title="Save with comment" form={form} close={close} + inFlight={form.formState.isSubmitting} submitText="Save" > name="comment" label="Comment" /> @@ -129,71 +133,23 @@ const SaveButton: FC<{ onSubmit: OnSubmit; disabled: boolean }> = ({ type Props = { // We have to pass the entire model here and not just content; // it's too hard to split the editing form into "content-type-specific" part and "generic model fields" part. - modelRef: EditSquiggleSnippetModel$key; + model: ModelFullDTO; forceVersionPicker?: boolean; }; export const EditSquiggleSnippetModel: FC = ({ - modelRef, + model, forceVersionPicker, }) => { - const { data: session } = useSession(); - - const model = useFragment( - graphql` - fragment EditSquiggleSnippetModel on Model { - id - slug - isEditable - ...EditRelativeValueExports_Model - ...SquiggleSnippetDraftDialog_Model - owner { - slug - } - lastRevisionWithBuild { - lastBuild { - runSeconds - } - } - currentRevision { - id - content { - __typename - ... on SquiggleSnippet { - id - code - version - seed - autorunMode - sampleCount - xyPointLength - } - } - exportNames - relativeValuesExports { - id - variableName - definition { - slug - owner { - slug - } - } - } - } - } - `, - modelRef - ); const revision = model.currentRevision; const router = useRouter(); - const content = extractFromGraphqlErrorUnion( - revision.content, - "SquiggleSnippet" - ); + const content = revision.squiggleSnippet; + if (!content) { + throw new Error("Unknown model type"); + } - const lastBuildSpeed = model.lastRevisionWithBuild?.lastBuild?.runSeconds; + const toast = useToast(); const seed = content.seed; @@ -210,51 +166,31 @@ export const EditSquiggleSnippetModel: FC = ({ }; }, [content, revision.relativeValuesExports]); - const { form, onSubmit, inFlight } = useMutationForm< + const { form, onSubmit, inFlight } = useSafeActionForm< SquiggleSnippetFormShape, - EditSquiggleSnippetModelMutation, - "UpdateSquiggleSnippetResult", + typeof updateSquiggleSnippetModelAction, { comment: string } >({ defaultValues: initialFormValues, - mutation: graphql` - mutation EditSquiggleSnippetModelMutation( - $input: MutationUpdateSquiggleSnippetModelInput! - ) { - result: updateSquiggleSnippetModel(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateSquiggleSnippetResult { - model { - ...EditSquiggleSnippetModel - } - } - } - } - `, - expectedTypename: "UpdateSquiggleSnippetResult", - formDataToVariables: (formData, extraData) => ({ - input: { - content: { - code: formData.code, - version, - seed: seed, - autorunMode: content.autorunMode, - sampleCount: content.sampleCount, - xyPointLength: content.xyPointLength, - }, - relativeValuesExports: formData.relativeValuesExports, - comment: extraData?.comment, - slug: model.slug, - owner: model.owner.slug, - }, - }), - confirmation: "Saved", - onCompleted() { + action: updateSquiggleSnippetModelAction, + onSuccess: () => { + toast("Saved", "confirmation"); draftUtils.discard(draftLocator); }, + formDataToInput: (formData, extraData) => ({ + content: { + code: formData.code, + version, + seed, + autorunMode: content.autorunMode, + sampleCount: content.sampleCount, + xyPointLength: content.xyPointLength, + }, + relativeValuesExports: formData.relativeValuesExports, + comment: extraData?.comment, + slug: model.slug, + owner: model.owner.slug, + }), }); // could version picker be part of the form? @@ -323,7 +259,11 @@ export const EditSquiggleSnippetModel: FC = ({ // Automatically turn off autorun, if the last build speed was > 5s. Note that this does not stop in the case of memory errors or similar. const autorunMode = content.autorunMode || - (lastBuildSpeed ? (lastBuildSpeed > 5 ? false : true) : true); + (model.lastBuildSeconds + ? model.lastBuildSeconds > 5 + ? false + : true + : true); // Build props for versioned SquigglePlayground first, since they might depend on the version we use, // and we want to populate them incrementally. @@ -331,7 +271,7 @@ export const EditSquiggleSnippetModel: FC = ({ typeof squiggle.components.SquigglePlayground >[0] = { defaultCode, - autorunMode: autorunMode, + autorunMode, sourceId: serializeSourceId({ owner: model.owner.slug, slug: model.slug, @@ -380,7 +320,7 @@ export const EditSquiggleSnippetModel: FC = ({ onSubmit(); }} items={variablesWithDefinitionsFields} - modelRef={model} + model={model} />
), @@ -413,14 +353,6 @@ export const EditSquiggleSnippetModel: FC = ({ ) : null; } - if ( - versionSupportsExports.propsByVersion<"SquigglePlayground">( - squiggle.version, - playgroundProps - ) - ) { - } - playgroundProps.environment = { sampleCount: content.sampleCount || SAMPLE_COUNT_DEFAULT, xyPointLength: content.xyPointLength || XY_POINT_LENGTH_DEFAULT, @@ -456,7 +388,7 @@ export const EditSquiggleSnippetModel: FC = ({ }: { importId: string; }) => ( - + ); diff --git a/packages/hub/src/app/models/[owner]/[slug]/FallbackLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/FallbackLayout.tsx index 49635f860c..f108d318a5 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/FallbackLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/FallbackLayout.tsx @@ -1,17 +1,21 @@ "use client"; -import { FC } from "react"; +import { FC, PropsWithChildren } from "react"; import { EntityLayout } from "@/components/EntityLayout"; import { ModelEntityNodes } from "./ModelEntityNodes"; -type Props = { +type Props = PropsWithChildren<{ username: string; slug: string; -}; +}>; -export const FallbackModelLayout: FC = ({ username, slug }) => { +export const FallbackModelLayout: FC = ({ + username, + slug, + children, +}) => { return ( = ({ username, slug }) => { }} /> } - /> + > + {children} + ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/FixModelUrlCasing.ts b/packages/hub/src/app/models/[owner]/[slug]/FixModelUrlCasing.ts deleted file mode 100644 index 3472a4eb84..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/FixModelUrlCasing.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { usePathname, useRouter } from "next/navigation"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { patchModelRoute } from "@/routes"; - -import { FixModelUrlCasing$key } from "@/__generated__/FixModelUrlCasing.graphql"; - -export const FixModelUrlCasingFragment = graphql` - fragment FixModelUrlCasing on Model { - id - slug - owner { - slug - } - } -`; - -export function useFixModelUrlCasing(modelRef: FixModelUrlCasing$key) { - const router = useRouter(); - const pathname = usePathname(); - const model = useFragment(FixModelUrlCasingFragment, modelRef); - - const patchedPathname = patchModelRoute({ - pathname, - slug: model.slug, - owner: model.owner.slug, - }); - if ( - patchedPathname && - patchedPathname !== pathname && - typeof window !== "undefined" - ) { - // delay to avoid React warnings - window.setTimeout(() => { - router.replace(patchedPathname); - }, 0); - } -} diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx deleted file mode 100644 index 5697bc180c..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; -import { clsx } from "clsx"; -import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; - -import { - Dropdown, - DropdownMenu, - DropdownMenuAsyncActionItem, - GlobeIcon, - LockIcon, -} from "@quri/ui"; - -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; - -import { ModelAccessControls$key } from "@/__generated__/ModelAccessControls.graphql"; -import { ModelAccessControlsMutation } from "@/__generated__/ModelAccessControlsMutation.graphql"; - -export const Fragment = graphql` - fragment ModelAccessControls on Model { - id - slug - isPrivate - isEditable - owner { - slug - } - } -`; - -function getIconComponent(isPrivate: boolean) { - return isPrivate ? LockIcon : GlobeIcon; -} - -export const Mutation = graphql` - mutation ModelAccessControlsMutation( - $input: MutationUpdateModelPrivacyInput! - ) { - result: updateModelPrivacy(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateModelPrivacyResult { - model { - id - isPrivate - } - } - } - } -`; - -export const UpdateModelPrivacyAction: FC<{ - modelRef: ModelAccessControls$key; - close(): void; -}> = ({ modelRef, close }) => { - const model = useFragment(Fragment, modelRef); - const [runMutation] = useAsyncMutation({ - mutation: Mutation, - expectedTypename: "UpdateModelPrivacyResult", - }); - - const act = () => - runMutation({ - variables: { - input: { - owner: model.owner.slug, - slug: model.slug, - isPrivate: !model.isPrivate, - }, - }, - }); - - return ( - - ); -}; - -export const ModelAccessControls: FC<{ modelRef: ModelAccessControls$key }> = ({ - modelRef, -}) => { - const model = useFragment(Fragment, modelRef); - - const Icon = getIconComponent(model.isPrivate); - - const body = ( - // TODO: copy-pasted from CacheMenu from relative-values, extract to or something -
- - {model.isPrivate ? "Private" : "Public"} -
- ); - - return model.isEditable ? ( - ( - - - - )} - > - {body} - - ) : ( - body - ); -}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx index 5ef879cc98..fe304c9b14 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx @@ -14,7 +14,7 @@ import { modelRoute, ownerRoute, variableRoute, -} from "@/routes"; +} from "@/lib/routes"; function hasTypename(owner: { __typename?: string; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx index f0e05c6b7f..9e7cf349cf 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx @@ -1,84 +1,31 @@ "use client"; import { FC, PropsWithChildren } from "react"; -import { graphql } from "relay-runtime"; import { CodeBracketSquareIcon, RectangleStackIcon, ShareIcon } from "@quri/ui"; import { EntityLayout } from "@/components/EntityLayout"; import { EntityTab } from "@/components/ui/EntityTab"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; +import { modelRevisionsRoute, modelRoute } from "@/lib/routes"; +import { getExportedVariableNames } from "@/models/clientUtils"; +import { ModelCardDTO } from "@/models/data/cards"; import { totalImportLength, type VariableRevision, VariablesDropdown, -} from "@/lib/VariablesDropdown"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { modelRevisionsRoute, modelRoute } from "@/routes"; +} from "@/variables/components/VariablesDropdown"; -import { useFixModelUrlCasing } from "./FixModelUrlCasing"; -import { ModelAccessControls } from "./ModelAccessControls"; import { ModelEntityNodes } from "./ModelEntityNodes"; +import { ModelPrivacyControls } from "./ModelPrivacyControls"; import { ModelSettingsButton } from "./ModelSettingsButton"; - -import { ModelLayoutQuery } from "@/__generated__/ModelLayoutQuery.graphql"; - -// Note that we have to do two GraphQL queries on most model pages: one for layout.tsx, and one for page.tsx. -const Query = graphql` - query ModelLayoutQuery($input: QueryModelInput!) { - result: model(input: $input) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on Model { - id - slug - isEditable - owner { - __typename - slug - } - ...FixModelUrlCasing - ...ModelAccessControls - ...ModelSettingsButton - variables { - id - variableName - currentRevision { - variableType - title - } - } - currentRevision { - id - exportNames - relativeValuesExports { - id - variableName - definition { - slug - } - } - } - } - } - } -`; +import { useFixModelUrlCasing } from "./useFixModelUrlCasing"; export const ModelLayout: FC< PropsWithChildren<{ - query: SerializablePreloadedQuery; + model: ModelCardDTO; + isEditable: boolean; }> -> = ({ query, children }) => { - const [{ result }] = usePageQuery(Query, query); - - const model = extractFromGraphqlErrorUnion(result, "Model"); - +> = ({ model, isEditable, children }) => { useFixModelUrlCasing(model); const modelUrl = modelRoute({ owner: model.owner.slug, slug: model.slug }); @@ -87,19 +34,20 @@ export const ModelLayout: FC< slug: model.slug, }); - const variableRevisions: VariableRevision[] = - model.currentRevision.exportNames.map((name) => { - const matchingVariable = model.variables.find( - (e) => e.variableName === name - ); + const variableRevisions: VariableRevision[] = getExportedVariableNames( + model.currentRevision.squiggleSnippet?.code ?? "" + ).map((name) => { + const matchingVariable = model.variables.find( + (e) => e.variableName === name + ); - return { - variableName: name, - variableType: - matchingVariable?.currentRevision?.variableType || undefined, - title: matchingVariable?.currentRevision?.title || undefined, - }; - }); + return { + variableName: name, + variableType: + matchingVariable?.currentRevision?.variableType || undefined, + title: matchingVariable?.currentRevision?.title || undefined, + }; + }); const relativeValuesExports = model.currentRevision.relativeValuesExports.map( ({ variableName, definition: { slug } }) => ({ @@ -117,7 +65,9 @@ export const ModelLayout: FC< } isFluid={true} - headerLeft={} + headerLeft={ + + } headerRight={ - {model.isEditable ? : null} + {isEditable ? : null} } > diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx new file mode 100644 index 0000000000..4f847b6832 --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx @@ -0,0 +1,67 @@ +"use client"; +import { clsx } from "clsx"; +import { FC } from "react"; + +import { Dropdown, DropdownMenu, GlobeIcon, LockIcon } from "@quri/ui"; + +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; +import { updateModelPrivacyAction } from "@/models/actions/updateModelPrivacyAction"; +import { ModelCardDTO } from "@/models/data/cards"; + +function getIconComponent(isPrivate: boolean) { + return isPrivate ? LockIcon : GlobeIcon; +} + +const UpdatePrivacyAction: FC<{ + model: ModelCardDTO; +}> = ({ model }) => { + return ( + + ); +}; + +export const ModelPrivacyControls: FC<{ + model: ModelCardDTO; + isEditable: boolean; +}> = ({ model, isEditable }) => { + const { isPrivate } = model; + + const Icon = getIconComponent(isPrivate); + + const body = ( + // TODO: copy-pasted from CacheMenu from relative-values, extract to or something +
+ + {isPrivate ? "Private" : "Public"} +
+ ); + + return isEditable ? ( + ( + + + + )} + > + {body} + + ) : ( + body + ); +}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx index d2b683a8e3..f92f25e845 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx @@ -1,38 +1,25 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { Cog8ToothIcon, Dropdown, DropdownMenu } from "@quri/ui"; import { EntityTab } from "@/components/ui/EntityTab"; +import { ModelCardDTO } from "@/models/data/cards"; import { DeleteModelAction } from "./DeleteModelAction"; import { MoveModelAction } from "./MoveModelAction"; import { UpdateModelSlugAction } from "./UpdateModelSlugAction"; -import { ModelSettingsButton$key } from "@/__generated__/ModelSettingsButton.graphql"; - export const ModelSettingsButton: FC<{ - model: ModelSettingsButton$key; -}> = ({ model: modelKey }) => { - const model = useFragment( - graphql` - fragment ModelSettingsButton on Model { - ...UpdateModelSlugAction - ...MoveModelAction - ...DeleteModelAction - } - `, - modelKey - ); - + model: ModelCardDTO; +}> = ({ model }) => { return ( ( - - + + )} > diff --git a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index 630ea7048f..25470b4401 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -1,88 +1,42 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { RightArrowIcon } from "@quri/ui"; import { SelectOwner, SelectOwnerOption } from "@/components/SelectOwner"; -import { MutationModalAction } from "@/components/ui/MutationModalAction"; -import { modelRoute } from "@/routes"; +import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; +import { modelRoute } from "@/lib/routes"; +import { moveModelAction } from "@/models/actions/moveModelAction"; +import { ModelCardDTO } from "@/models/data/cards"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; -import { MoveModelAction$key } from "@/__generated__/MoveModelAction.graphql"; -import { MoveModelActionMutation } from "@/__generated__/MoveModelActionMutation.graphql"; - -const Mutation = graphql` - mutation MoveModelActionMutation($input: MutationMoveModelInput!) { - result: moveModel(input: $input) { - __typename - ... on BaseError { - message - } - ... on MoveModelResult { - model { - id - slug - owner { - __typename - id - slug - } - } - } - } - } -`; - type FormShape = { owner: SelectOwnerOption }; type Props = { - model: MoveModelAction$key; - close(): void; + model: ModelCardDTO; }; -export const MoveModelAction: FC = ({ model: modelKey, close }) => { - const model = useFragment( - graphql` - fragment MoveModelAction on Model { - slug - owner { - __typename - id - slug - } - } - `, - modelKey - ); - +export const MoveModelAction: FC = ({ model }) => { const router = useRouter(); return ( - - mutation={Mutation} - expectedTypename="MoveModelResult" - formDataToVariables={(data) => ({ - input: { - oldOwner: model.owner.slug, - newOwner: data.owner.slug, - slug: model.slug, - }, - })} - initialFocus="owner" + + title="Change Owner" + modalTitle={`Change owner for ${model.owner.slug}/${model.slug}`} + submitText="Save" defaultValues={{ // __typename from fragment is string, while SelectOwner requires 'User' | 'Group' union, // so we have to explicitly recast owner: model.owner as SelectOwnerOption, }} - submitText="Save" - close={close} - title="Change Owner" - icon={RightArrowIcon} - modalTitle={`Change owner for ${model.owner.slug}/${model.slug}`} - onCompleted={({ model: newModel }) => { + action={moveModelAction} + formDataToInput={(data) => ({ + oldOwner: model.owner.slug, + owner: { slug: data.owner.slug }, + slug: model.slug, + })} + onSuccess={({ model: newModel }) => { draftUtils.rename( modelToDraftLocator(model), modelToDraftLocator(newModel) @@ -91,6 +45,9 @@ export const MoveModelAction: FC = ({ model: modelKey, close }) => { modelRoute({ owner: newModel.owner.slug, slug: newModel.slug }) ); }} + icon={RightArrowIcon} + initialFocus="owner" + blockOnSuccess > {() => (
@@ -100,6 +57,6 @@ export const MoveModelAction: FC = ({ model: modelKey, close }) => { name="owner" label="New owner" myOnly />
)} - + ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx b/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx index a82de66400..e8f6b5ae4d 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx @@ -1,14 +1,12 @@ import { FC, PropsWithChildren, useState } from "react"; -import { graphql, useFragment } from "react-relay"; import { Button, Modal } from "@quri/ui"; -import { useClientOnlyRender } from "@/hooks/useClientOnlyRender"; +import { useClientOnlyRender } from "@/lib/hooks/useClientOnlyRender"; +import { ModelFullDTO } from "@/models/data/full"; import { SquiggleSnippetFormShape } from "./EditSquiggleSnippetModel"; -import { SquiggleSnippetDraftDialog_Model$key } from "@/__generated__/SquiggleSnippetDraftDialog_Model.graphql"; - export type Draft = { formState: SquiggleSnippetFormShape; version: string; @@ -98,22 +96,7 @@ export const draftUtils = { }, }; -export function useDraftLocator( - modelRef: SquiggleSnippetDraftDialog_Model$key -) { - const model = useFragment( - graphql` - fragment SquiggleSnippetDraftDialog_Model on Model { - id - slug - owner { - slug - } - } - `, - modelRef - ); - +export function useDraftLocator(model: ModelFullDTO) { const draftLocator: DraftLocator = { ownerSlug: model.owner.slug, modelSlug: model.slug, diff --git a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index 0d7d339ee9..5cd77d72b1 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -1,86 +1,38 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { EditIcon } from "@quri/ui"; -import { MutationModalAction } from "@/components/ui/MutationModalAction"; +import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { modelRoute } from "@/routes"; +import { modelRoute } from "@/lib/routes"; +import { updateModelSlugAction } from "@/models/actions/updateModelSlugAction"; +import { ModelCardDTO } from "@/models/data/cards"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; -import { UpdateModelSlugAction$key } from "@/__generated__/UpdateModelSlugAction.graphql"; -import { UpdateModelSlugActionMutation } from "@/__generated__/UpdateModelSlugActionMutation.graphql"; - -const Mutation = graphql` - mutation UpdateModelSlugActionMutation( - $input: MutationUpdateModelSlugInput! - ) { - result: updateModelSlug(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateModelSlugResult { - model { - id - slug - owner { - slug - } - } - } - } - } -`; - type Props = { - model: UpdateModelSlugAction$key; + model: ModelCardDTO; close(): void; }; type FormShape = { slug: string }; -export const UpdateModelSlugAction: FC = ({ - model: modelKey, - close, -}) => { - const model = useFragment( - graphql` - fragment UpdateModelSlugAction on Model { - slug - owner { - __typename - id - slug - } - } - `, - modelKey - ); - +export const UpdateModelSlugAction: FC = ({ model, close }) => { const router = useRouter(); return ( - + title="Rename" icon={EditIcon} - mutation={Mutation} - expectedTypename="UpdateModelSlugResult" - formDataToVariables={(data) => ({ - input: { - owner: model.owner.slug, - oldSlug: model.slug, - newSlug: data.slug, - }, + action={updateModelSlugAction} + defaultValues={{ slug: model.slug }} + formDataToInput={(data) => ({ + owner: model.owner.slug, + oldSlug: model.slug, + slug: data.slug, })} - onCompleted={({ model: newModel }) => { + onSuccess={({ model: newModel }) => { draftUtils.rename( modelToDraftLocator(model), modelToDraftLocator(newModel) @@ -92,7 +44,6 @@ export const UpdateModelSlugAction: FC = ({ submitText="Save" modalTitle={`Rename ${model.owner.slug}/${model.slug}`} initialFocus="slug" - close={close} > {() => (
@@ -102,6 +53,6 @@ export const UpdateModelSlugAction: FC = ({ name="slug" label="New slug" />
)} - + ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx index d327cf1680..71ec8671a3 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx @@ -1,32 +1,43 @@ import { Metadata } from "next"; +import { notFound } from "next/navigation"; import { PropsWithChildren, Suspense } from "react"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { loadModelCard } from "@/models/data/cards"; +import { isModelEditable } from "@/models/data/helpers"; import { FallbackModelLayout } from "./FallbackLayout"; import { ModelLayout } from "./ModelLayout"; -import ModelLayoutQueryNode, { - ModelLayoutQuery, -} from "@/__generated__/ModelLayoutQuery.graphql"; - type Props = PropsWithChildren<{ params: Promise<{ owner: string; slug: string }>; }>; async function LoadedLayout({ params, children }: Props) { const { owner, slug } = await params; - const query = await loadPageQuery(ModelLayoutQueryNode, { - input: { owner, slug }, - }); + const model = await loadModelCard({ owner, slug }); + if (!model) { + notFound(); + } + + const isEditable = await isModelEditable(model); - return {children}; + return ( + + {children} + + ); } export default async function Layout({ params, children }: Props) { const { owner, slug } = await params; return ( - }> + + {children} + + } + > {children} ); diff --git a/packages/hub/src/app/models/[owner]/[slug]/loading.tsx b/packages/hub/src/app/models/[owner]/[slug]/loading.tsx deleted file mode 100644 index 732a9336af..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/loading.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Skeleton from "react-loading-skeleton"; - -import { FullLayoutWithPadding } from "@/components/layout/FullLayoutWithPadding"; - -export default function Loading() { - return ( - - - - ); -} diff --git a/packages/hub/src/app/models/[owner]/[slug]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/page.tsx index a72ec311e0..5c02ef44d8 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/page.tsx @@ -1,24 +1,56 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { notFound } from "next/navigation"; +import { Suspense } from "react"; +import Skeleton from "react-loading-skeleton"; -import { EditModelPage } from "./EditModelPage"; +import { loadModelFull } from "@/models/data/full"; -import QueryNode, { - EditModelPageQuery, -} from "@/__generated__/EditModelPageQuery.graphql"; +import { EditSquiggleSnippetModel } from "./EditSquiggleSnippetModel"; type Props = { params: Promise<{ owner: string; slug: string }>; }; -export default async function Page({ params }: Props) { +async function InnerPage({ params }: Props) { const { owner, slug } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, - }); + const model = await loadModelFull({ owner, slug }); + if (!model) { + notFound(); + } + + const { contentType } = model.currentRevision; + switch (contentType) { + case "SquiggleSnippet": + return ( +
+ +
+ ); + default: + return
Unknown model type {contentType}
; + } +} + +const Loading = () => { + return ( +
+
+ +
+
+ +
+
+ ); +}; + +export default async function Page({ params }: Props) { return (
- + {/* Intentionally not using loading.tsx here, because this route has sub-routes where that doesn't make sense. */} + }> + +
); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx index e16eac570a..fd9d442b0e 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx @@ -1,57 +1,28 @@ "use client"; import { FC } from "react"; -import { graphql } from "react-relay"; -import { DropdownMenuAsyncActionItem, RefreshIcon } from "@quri/ui"; +import { RefreshIcon, useToast } from "@quri/ui"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; - -import { BuildRelativeValuesCacheActionMutation } from "@/__generated__/BuildRelativeValuesCacheActionMutation.graphql"; - -export const Mutation = graphql` - mutation BuildRelativeValuesCacheActionMutation( - $input: MutationBuildRelativeValuesCacheInput! - ) { - result: buildRelativeValuesCache(input: $input) { - __typename - ... on BaseError { - message - } - ... on BuildRelativeValuesCacheResult { - relativeValuesExport { - id - cache { - firstItem - secondItem - resultJSON - errorString - } - } - } - } - } -`; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; +import { buildRelativeValuesCacheAction } from "@/relative-values/actions/buildRelativeValuesCacheAction"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; export const BuildRelativeValuesCacheAction: FC<{ - exportId: string; - close(): void; -}> = ({ exportId, close }) => { + relativeValuesExport: RelativeValuesExportFullDTO; +}> = ({ relativeValuesExport }) => { // TODO - fill cache in ModelEvaluator and re-render - const [runMutation] = - useAsyncMutation({ - mutation: Mutation, - confirmation: "Cache filled", - expectedTypename: "BuildRelativeValuesCacheResult", - }); - - const act = () => runMutation({ variables: { input: { exportId } } }); + const toast = useToast(); return ( - { + toast("Cache filled", "confirmation"); + }} + invariant={1} // close is controlled by the parent /> ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx index 7bcd13c7da..3e83870a78 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx @@ -1,62 +1,28 @@ "use strict"; import { FC } from "react"; -import { graphql } from "react-relay"; -import { DropdownMenuAsyncActionItem, TrashIcon } from "@quri/ui"; +import { TrashIcon, useToast } from "@quri/ui"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; - -import { ClearRelativeValuesCacheActionMutation } from "@/__generated__/ClearRelativeValuesCacheActionMutation.graphql"; - -export const Mutation = graphql` - mutation ClearRelativeValuesCacheActionMutation( - $input: MutationClearRelativeValuesCacheInput! - ) { - result: clearRelativeValuesCache(input: $input) { - __typename - ... on BaseError { - message - } - ... on ClearRelativeValuesCacheResult { - relativeValuesExport { - id - cache { - firstItem - secondItem - resultJSON - errorString - } - } - } - } - } -`; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; +import { clearRelativeValuesCacheAction } from "@/relative-values/actions/clearRelativeValuesCacheAction"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; export const ClearRelativeValuesCacheAction: FC<{ - exportId: string; - close(): void; -}> = ({ exportId, close }) => { + relativeValuesExport: RelativeValuesExportFullDTO; +}> = ({ relativeValuesExport }) => { // TODO - clear cache in ModelEvaluator and re-render - const [runMutation] = - useAsyncMutation({ - mutation: Mutation, - confirmation: "Cache cleared", - expectedTypename: "ClearRelativeValuesCacheResult", - }); - - const act = () => - runMutation({ - variables: { - input: { exportId }, - }, - }); + const toast = useToast(); return ( - { + toast("Cache cleared", "confirmation"); + }} + invariant={1} // close is controlled by the parent /> ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx index 1a2df16d8f..ff22a4c5e1 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx @@ -1,7 +1,6 @@ "use client"; import { clsx } from "clsx"; import { FC, ReactElement } from "react"; -import { useFragment } from "react-relay"; import { CheckIcon, @@ -11,29 +10,24 @@ import { XIcon, } from "@quri/ui"; -import { RelativeValuesDefinitionRevisionFragment } from "@/relative-values/components/RelativeValuesDefinitionRevision"; +import { CloseDropdownOnInvariantChange } from "@/components/ui/CloseDropdownOnInvariantChange"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; import { BuildRelativeValuesCacheAction } from "./BuildRelativeValuesCacheAction"; import { ClearRelativeValuesCacheAction } from "./ClearRelativeValuesCacheAction"; -import { RelativeValuesDefinitionRevision$key } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; -import { RelativeValuesExport$data } from "@/__generated__/RelativeValuesExport.graphql"; - export const CacheMenu: FC<{ - relativeValuesExport: RelativeValuesExport$data; + relativeValuesExport: RelativeValuesExportFullDTO; + definitionRevision: RelativeValuesDefinitionFullDTO["currentRevision"]; isEditable: boolean; -}> = ({ relativeValuesExport, isEditable }) => { - const definition = useFragment( - RelativeValuesDefinitionRevisionFragment, - relativeValuesExport.definition.currentRevision - ); - +}> = ({ relativeValuesExport, definitionRevision, isEditable }) => { const isEmpty = relativeValuesExport.cache.length === 0; const fullyCached = !isEmpty && relativeValuesExport.cache.length >= - definition.items.length * definition.items.length; + definitionRevision.items.length * definitionRevision.items.length; const internals = (
( { + render={() => { return ( + {isEmpty ? "Not cached" : `${relativeValuesExport.cache.length}/${ - definition.items.length * definition.items.length + definitionRevision.items.length * + definitionRevision.items.length } pairs cached`} {!fullyCached && ( )} {isEmpty ? null : ( )} diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesExport.ts b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesExport.ts deleted file mode 100644 index 670e011d9e..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesExport.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { graphql } from "relay-runtime"; - -// Used in ModelEvaluator and other relative-values functions. -// Since those functions are not components or hooks, we pass around a RelativeValuesExport$data value. -export const RelativeValuesExport = graphql` - fragment RelativeValuesExport on RelativeValuesExport { - id - ...RelativeValuesExportItem - definition { - slug - owner { - slug - } - currentRevision { - ...RelativeValuesDefinitionRevision - } - } - cache { - firstItem - secondItem - resultJSON - errorString - } - } -`; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx index 328a74d7e1..d6880a84fa 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx @@ -2,126 +2,32 @@ import { FC, PropsWithChildren, useEffect, useState } from "react"; import Skeleton from "react-loading-skeleton"; -import { graphql, useFragment } from "react-relay"; import { result } from "@quri/squiggle-lang"; -import { - Bars4Icon, - LinkIcon, - ScaleIcon, - ScatterPlotIcon, - TableCellsIcon, -} from "@quri/ui"; -import { StyledLink } from "@/components/ui/StyledLink"; -import { StyledTabLink } from "@/components/ui/StyledTabLink"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { RelativeValuesDefinitionRevisionFragment } from "@/relative-values/components/RelativeValuesDefinitionRevision"; import { RelativeValuesProvider } from "@/relative-values/components/views/RelativeValuesProvider"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { - modelForRelativeValuesExportRoute, - relativeValuesRoute, -} from "@/routes"; - -import { CacheMenu } from "./CacheMenu"; -import { RelativeValuesExport } from "./RelativeValuesExport"; - -import { RelativeValuesDefinitionRevision$key } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; -import { RelativeValuesExport$key } from "@/__generated__/RelativeValuesExport.graphql"; -import { RelativeValuesModelLayoutQuery } from "@/__generated__/RelativeValuesModelLayoutQuery.graphql"; export const RelativeValuesModelLayout: FC< PropsWithChildren<{ - query: SerializablePreloadedQuery; + code: string; variableName: string; + cache: RelativeValuesExportFullDTO["cache"]; + definitionRevision: RelativeValuesDefinitionFullDTO["currentRevision"]; }> -> = ({ query, variableName, children }) => { - const [{ model: result }] = usePageQuery( - graphql` - # used in ModelEvaluator - query RelativeValuesModelLayoutQuery( - $input: QueryModelInput! - $forRelativeValues: ModelRevisionForRelativeValuesInput! - ) { - model(input: $input) { - __typename - ... on Model { - id - slug - isEditable - ...EditRelativeValueExports_Model - owner { - slug - } - currentRevision { - id - content { - __typename - ... on SquiggleSnippet { - id - code - version - } - } - - forRelativeValues(input: $forRelativeValues) { - __typename - ... on BaseError { - message - } - ... on RelativeValuesExport { - ...RelativeValuesExport - } - } - } - } - } - } - `, - query - ); - const model = extractFromGraphqlErrorUnion(result, "Model"); - const revision = model.currentRevision; - - const content = extractFromGraphqlErrorUnion( - revision.content, - "SquiggleSnippet" - ); - - const forRelativeValuesKey = extractFromGraphqlErrorUnion( - revision.forRelativeValues, - "RelativeValuesExport" - ); - - const forRelativeValues = useFragment( - RelativeValuesExport, - forRelativeValuesKey - ); - - const definition = forRelativeValues.definition; - - const definitionRevision = useFragment( - RelativeValuesDefinitionRevisionFragment, - forRelativeValues.definition.currentRevision - ); - +> = ({ code, variableName, cache, definitionRevision, children }) => { const [evaluatorResult, setEvaluatorResult] = useState< result | undefined >(); useEffect(() => { // ModelEvaluator.create is async because SqProject.run is async - ModelEvaluator.create( - content.code, - variableName, - forRelativeValues.cache - ).then(setEvaluatorResult); - }, [content.code, variableName, forRelativeValues]); + ModelEvaluator.create(code, variableName, cache).then(setEvaluatorResult); + }, [code, variableName, cache]); - const body = evaluatorResult ? ( + return evaluatorResult ? ( evaluatorResult.ok ? ( ); - - const definitionLink = ( - - - {`${definition.owner.slug}/${definition.slug}`} - - ); - - return ( -
-
-
- -
- {variableName} -
-
-
- {definitionLink} - - - - - - -
-
- {body} -
- ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx new file mode 100644 index 0000000000..046c39363a --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { FC } from "react"; + +import { Bars4Icon, ScatterPlotIcon, TableCellsIcon } from "@quri/ui"; + +import { StyledTabLink } from "@/components/ui/StyledTabLink"; +import { modelForRelativeValuesExportRoute } from "@/lib/routes"; +import { ModelCardDTO } from "@/models/data/cards"; + +// must be a client component because we can't pass icons from server components to client components +export const RelativeValuesTabs: FC<{ + model: ModelCardDTO; + variableName: string; +}> = ({ model, variableName }) => { + return ( + + + + + + ); +}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx index 9a888646db..715a7b7e11 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx @@ -1,12 +1,18 @@ +import { notFound } from "next/navigation"; import { PropsWithChildren } from "react"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { LinkIcon, ScaleIcon } from "@quri/ui"; -import { RelativeValuesModelLayout } from "./RelativeValuesModelLayout"; +import { StyledLink } from "@/components/ui/StyledLink"; +import { relativeValuesRoute } from "@/lib/routes"; +import { loadModelCard } from "@/models/data/cards"; +import { isModelEditable } from "@/models/data/helpers"; +import { loadRelativeValuesDefinitionFull } from "@/relative-values/data/full"; +import { loadRelativeValuesExportFullFromModelRevision } from "@/relative-values/data/fullExport"; -import QueryNode, { - RelativeValuesModelLayoutQuery, -} from "@/__generated__/RelativeValuesModelLayoutQuery.graphql"; +import { CacheMenu } from "./CacheMenu"; +import { RelativeValuesModelLayout } from "./RelativeValuesModelLayout"; +import { RelativeValuesTabs } from "./Tabs"; export default async function Layout({ params, @@ -15,19 +21,83 @@ export default async function Layout({ params: Promise<{ owner: string; slug: string; variableName: string }>; }>) { const { owner, slug, variableName } = await params; - const query = await loadPageQuery(QueryNode, { - input: { - owner, - slug, - }, - forRelativeValues: { + + // sleep + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const model = await loadModelCard({ owner, slug }); + if (!model) { + notFound(); + } + + const isEditable = await isModelEditable(model); + + const revision = model.currentRevision; + + const relativeValuesExport = + await loadRelativeValuesExportFullFromModelRevision({ + modelRevisionId: revision.id, variableName, - }, + }); + + if (!relativeValuesExport) { + notFound(); + } + + const definition = await loadRelativeValuesDefinitionFull({ + owner: relativeValuesExport.definition.owner, + slug: relativeValuesExport.definition.slug, }); + if (!definition) { + notFound(); + } + + const content = revision.squiggleSnippet; + if (!content) { + throw new Error("No SquiggleSnippet content"); + } + + const definitionLink = ( + + + {`${definition.owner.slug}/${definition.slug}`} + + ); + return ( - - {children} - +
+
+
+ +
+ {variableName} +
+
+
+ {definitionLink} + + +
+
+ + {children} + +
); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx index 55032b646c..e7b1977860 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx @@ -1,62 +1,21 @@ "use client"; -import { ModelRevisionsListQuery } from "@gen/ModelRevisionsListQuery.graphql"; import { format } from "date-fns"; import { FC } from "react"; -import { useFragment, usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { LoadMore } from "@/components/LoadMore"; import { StyledLink } from "@/components/ui/StyledLink"; import { UsernameLink } from "@/components/UsernameLink"; -import { commonDateFormat } from "@/lib/common"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { modelRevisionRoute } from "@/routes"; - -import { ModelRevisionsList$key } from "@/__generated__/ModelRevisionsList.graphql"; -import { ModelRevisionsList_model$key } from "@/__generated__/ModelRevisionsList_model.graphql"; -import { ModelRevisionsList_revision$key } from "@/__generated__/ModelRevisionsList_revision.graphql"; +import { commonDateFormat } from "@/lib/constants"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { modelRevisionRoute } from "@/lib/routes"; +import { Paginated } from "@/lib/types"; +import { ModelCardDTO } from "@/models/data/cards"; +import { ModelRevisionDTO } from "@/models/data/revisions"; const ModelRevisionItem: FC<{ - modelRef: ModelRevisionsList_model$key; - revisionRef: ModelRevisionsList_revision$key; -}> = ({ modelRef, revisionRef }) => { - const revision = useFragment( - graphql` - fragment ModelRevisionsList_revision on ModelRevision { - id - createdAtTimestamp - buildStatus - author { - username - } - comment - variableRevisions { - id - } - lastBuild { - errors - runSeconds - } - } - `, - revisionRef - ); - - const model = useFragment( - graphql` - fragment ModelRevisionsList_model on Model { - id - slug - owner { - slug - } - } - `, - modelRef - ); - + model: ModelCardDTO; + revision: ModelRevisionDTO; +}> = ({ model, revision }) => { return (
@@ -67,7 +26,7 @@ const ModelRevisionItem: FC<{ revisionId: revision.id, })} > - {format(new Date(revision.createdAtTimestamp), commonDateFormat)} + {format(revision.createdAt, commonDateFormat)} {revision.author ? ( <> @@ -94,67 +53,23 @@ const ModelRevisionItem: FC<{ }; export const ModelRevisionsList: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ model: result }] = usePageQuery( - graphql` - query ModelRevisionsListQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - id - ...ModelRevisionsList_model - ...ModelRevisionsList - } - } - } - `, - query - ); - - const modelRef = extractFromGraphqlErrorUnion(result, "Model"); - - const { - data: { revisions }, - loadNext, - } = usePaginationFragment( - graphql` - fragment ModelRevisionsList on Model - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "ModelRevisionsListPaginationQuery") { - revisions(first: $count, after: $cursor) - @connection(key: "ModelRevisionsList_revisions") { - edges { - node { - id - ...ModelRevisionsList_revision - } - } - pageInfo { - hasNextPage - } - } - } - `, - modelRef - ); + page: Paginated; + model: ModelCardDTO; +}> = ({ page: initialPage, model }) => { + const { items: revisions, loadNext } = usePaginator(initialPage); return (
-
Revision history
- {revisions.edges.map((edge) => ( + {revisions.map((revision) => ( ))}
- {revisions.pageInfo.hasNextPage && } + {loadNext && }
); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx index 898e8badb1..3cc757cc41 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx @@ -1,103 +1,28 @@ "use client"; -import { ModelRevisionViewQuery } from "@gen/ModelRevisionViewQuery.graphql"; -import { format } from "date-fns"; import { FC, use } from "react"; -import { graphql } from "relay-runtime"; -import { CommentIcon } from "@quri/ui"; import { useAdjustSquiggleVersion, versionedSquigglePackages, } from "@quri/versioned-squiggle-components"; -import { StyledLink } from "@/components/ui/StyledLink"; -import { commonDateFormat } from "@/lib/common"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { modelRoute } from "@/routes"; -import { getHubLinker } from "@/squiggle/components/linker"; - -const Query = graphql` - query ModelRevisionViewQuery($input: QueryModelInput!, $revisionId: ID!) { - result: model(input: $input) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on Model { - id - slug - owner { - slug - } - revision(id: $revisionId) { - comment - createdAtTimestamp - content { - __typename - ... on SquiggleSnippet { - code - version - } - } - } - } - } - } -`; +import { ModelRevisionFullDTO } from "@/models/data/fullRevision"; +import { getHubLinker } from "@/squiggle/linker"; export const ModelRevisionView: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ result }] = usePageQuery(Query, query); - const model = extractFromGraphqlErrorUnion(result, "Model"); - - const modelUrl = modelRoute({ - owner: model.owner.slug, - slug: model.slug, - }); - - const typename = model.revision.content.__typename; - if (typename !== "SquiggleSnippet") { - throw new Error(`Unknown model type ${typename}`); - } - + revision: ModelRevisionFullDTO; +}> = ({ revision }) => { const checkedVersion = useAdjustSquiggleVersion( - model.revision.content.version + revision.squiggleSnippet.version ); const squiggle = use(versionedSquigglePackages(checkedVersion)); return ( -
-
-
-
- Version from{" "} - {format(model.revision.createdAtTimestamp, commonDateFormat)}.{" "} - Squiggle{" "} - {model.revision.content.version}. -
-
- Go to latest version -
- {model.revision.comment ? ( -
- -
{model.revision.comment}
-
- ) : null} -
-
- -
+ ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx index 439962781b..e92a2ff6d6 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx @@ -1,10 +1,14 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { format } from "date-fns"; +import { notFound } from "next/navigation"; -import { ModelRevisionView } from "./ModelRevisionView"; +import { CommentIcon } from "@quri/ui"; + +import { StyledLink } from "@/components/ui/StyledLink"; +import { commonDateFormat } from "@/lib/constants"; +import { modelRoute } from "@/lib/routes"; +import { loadModelRevisionFull } from "@/models/data/fullRevision"; -import QueryNode, { - ModelRevisionViewQuery, -} from "@/__generated__/ModelRevisionViewQuery.graphql"; +import { ModelRevisionView } from "./ModelRevisionView"; export default async function ModelPage({ params, @@ -12,10 +16,44 @@ export default async function ModelPage({ params: Promise<{ owner: string; slug: string; revisionId: string }>; }) { const { owner, slug, revisionId } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, + + const revision = await loadModelRevisionFull({ + owner, + slug, revisionId, }); - return ; + if (!revision) { + notFound(); + } + + const modelUrl = modelRoute({ + owner, + slug, + }); + + return ( +
+
+
+
+ Version from{" "} + {format(revision.createdAt, commonDateFormat)}.{" "} + Squiggle{" "} + {revision.squiggleSnippet.version}. +
+
+ Go to latest version +
+ {revision.comment ? ( +
+ +
{revision.comment}
+
+ ) : null} +
+
+ +
+ ); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx index 1e9c63f9d7..f351dea506 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx @@ -1,25 +1,39 @@ +import { notFound } from "next/navigation"; +import { Suspense } from "react"; +import Skeleton from "react-loading-skeleton"; + import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { loadModelCard } from "@/models/data/cards"; +import { loadModelRevisions } from "@/models/data/revisions"; import { ModelRevisionsList } from "./ModelRevisionsList"; -import QueryNode, { - ModelRevisionsListQuery, -} from "@/__generated__/ModelRevisionsListQuery.graphql"; - -export default async function ModelPage({ +async function InnerRevisionsPage({ params, }: { params: Promise<{ owner: string; slug: string }>; }) { const { owner, slug } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, - }); + const page = await loadModelRevisions({ owner, slug }); + const model = await loadModelCard({ owner, slug }); + if (!model) { + notFound(); + } + + return ; +} +export default async function ModelPage({ + params, +}: { + params: Promise<{ owner: string; slug: string }>; +}) { return ( - +
Revision history
+ }> + +
); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts new file mode 100644 index 0000000000..b30e775153 --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts @@ -0,0 +1,25 @@ +import { usePathname, useRouter } from "next/navigation"; + +import { patchModelRoute } from "@/lib/routes"; +import { ModelCardDTO } from "@/models/data/cards"; + +export function useFixModelUrlCasing(model: ModelCardDTO) { + const router = useRouter(); + const pathname = usePathname(); + + const patchedPathname = patchModelRoute({ + pathname, + slug: model.slug, + owner: model.owner.slug, + }); + if ( + patchedPathname && + patchedPathname !== pathname && + typeof window !== "undefined" + ) { + // delay to avoid React warnings + window.setTimeout(() => { + router.replace(patchedPathname); + }, 0); + } +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariablePage.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariablePage.tsx deleted file mode 100644 index 3a93c40278..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariablePage.tsx +++ /dev/null @@ -1,211 +0,0 @@ -"use client"; -import clsx from "clsx"; -import { format } from "date-fns"; -import { FC, useState } from "react"; -import { FaClock, FaMinusCircle } from "react-icons/fa"; -import { graphql, usePaginationFragment } from "react-relay"; -import { FragmentRefs } from "relay-runtime"; - -import { CheckIcon, XIcon } from "@quri/ui"; - -import { LoadMore } from "@/components/LoadMore"; -import { Link } from "@/components/ui/Link"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { exportTypeIcon } from "@/lib/typeIcon"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { SquiggleVariableRevisionPage } from "./SquiggleVariableRevisionPage"; - -import { - VariablePage$data, - VariablePage$key, -} from "@/__generated__/VariablePage.graphql"; -import { VariablePageQuery } from "@/__generated__/VariablePageQuery.graphql"; - -const buildStatusIcon = (status: string) => { - switch (status) { - case "Success": - return ; - case "Failure": - return ; - case "Pending": - return ; - case "Skipped": - return ; - } -}; - -type VariableRevisions = VariablePage$data["revisions"]; -const VariableRevisionsPanel: FC<{ - revisions: VariableRevisions; - selected: string; - changeId: (id: string) => void; - loadNext?: (count: number) => void; -}> = ({ revisions, selected, changeId, loadNext }) => { - return ( -
-

- Revisions -

-
- {revisions.edges.map(({ node: revision }) => { - const Icon = exportTypeIcon(revision.variableType || ""); - return ( -
-
- changeId(revision.id)} - > - {format( - new Date(revision.modelRevision.createdAtTimestamp), - "MMM dd h:mm a" - )} - -
-
- -
-
- {buildStatusIcon(revision.modelRevision.buildStatus)} -
-
- ); - })} -
- {loadNext && } -
- ); -}; - -export const VariablePageBody: FC<{ - variableName: string; - result: { - readonly __typename: "Variable"; - readonly id: string; - readonly variableName: string; - readonly " $fragmentSpreads": FragmentRefs<"VariablePage">; - }; -}> = ({ result, variableName }) => { - const variable = extractFromGraphqlErrorUnion(result, "Variable"); - - const { - data: { revisions }, - loadNext, - } = usePaginationFragment( - graphql` - fragment VariablePage on Variable - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "VariablePagePaginationQuery") { - revisions(first: $count, after: $cursor) - @connection(key: "VariablePage_revisions") { - edges { - node { - id - variableName - variableType - modelRevision { - id - createdAtTimestamp - content { - __typename - ...SquiggleVariableRevisionPage - } - buildStatus - } - } - } - pageInfo { - hasNextPage - } - } - } - `, - variable - ); - - const [selected, changeId] = useState( - revisions.edges.at(0)?.node.id ?? null - ); - - if (selected === null) { - return
No revisions found. They should be built shortly.
; - } - - const content = revisions.edges.find((edge) => edge.node.id === selected) - ?.node.modelRevision.content; - - if (content) { - switch (content.__typename) { - case "SquiggleSnippet": { - return ( -
-
- -
- -
- ); - } - default: - return
Unknown model type {content.__typename}
; - } - } -}; - -export const VariablePage: FC<{ - params: { - owner: string; - slug: string; - variableName: string; - }; - query: SerializablePreloadedQuery; -}> = ({ query, params }) => { - const [{ variable: result }] = usePageQuery( - graphql` - query VariablePageQuery($input: QueryVariableInput!) { - variable(input: $input) { - __typename - ... on Variable { - id - variableName - ...VariablePage - } - } - } - `, - query - ); - - if (result.__typename !== "Variable") { - return
No revisions found. They should be built shortly.
; - } - - return ( - - ); -}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx new file mode 100644 index 0000000000..a857388a7a --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx @@ -0,0 +1,87 @@ +"use client"; + +import clsx from "clsx"; +import { format } from "date-fns"; +import { usePathname } from "next/navigation"; +import { FC } from "react"; +import { FaClock, FaMinusCircle } from "react-icons/fa"; + +import { CheckIcon, XIcon } from "@quri/ui"; + +import { LoadMore } from "@/components/LoadMore"; +import { Link } from "@/components/ui/Link"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { variableRevisionRoute } from "@/lib/routes"; +import { exportTypeIcon } from "@/lib/typeIcon"; +import { Paginated } from "@/lib/types"; +import { VariableRevisionDTO } from "@/variables/data/variableRevisions"; + +const buildStatusIcon = (status: string) => { + switch (status) { + case "Success": + return ; + case "Failure": + return ; + case "Pending": + return ; + case "Skipped": + return ; + } +}; + +export const VariableRevisionsPanel: FC<{ + page: Paginated; + owner: string; + modelSlug: string; + variableName: string; +}> = ({ page: initialPage, owner, modelSlug, variableName }) => { + const { items: revisions, loadNext } = usePaginator(initialPage); + + const pathname = usePathname(); + + return ( +
+

+ Revisions +

+
+ {revisions.map((revision) => { + const Icon = exportTypeIcon(revision.variableType); + const link = variableRevisionRoute({ + owner, + modelSlug, + variableName, + revisionId: revision.id, + }); + + return ( +
+
+ + {format(revision.modelRevision.createdAt, "MMM dd h:mm a")} + +
+
+ +
+
+ {buildStatusIcon(revision.modelRevision.buildStatus)} +
+
+ ); + })} +
+ {loadNext && } +
+ ); +}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx new file mode 100644 index 0000000000..11cc682167 --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx @@ -0,0 +1,33 @@ +import { PropsWithChildren } from "react"; + +import { loadVariableRevisions } from "@/variables/data/variableRevisions"; + +import { VariableRevisionsPanel } from "./VariableRevisionsPanel"; + +type Props = { + params: Promise<{ owner: string; slug: string; variableName: string }>; +}; + +export default async function VariableLayout({ + children, + params, +}: PropsWithChildren) { + const { owner, slug, variableName } = await params; + const revisions = await loadVariableRevisions({ owner, slug, variableName }); + + if (!revisions.items.length) { + return
No revisions found. They should be built shortly.
; + } + + return ( +
+
{children}
+ +
+ ); +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx index 59b7ee0586..f1b7aa5a94 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx @@ -1,10 +1,9 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { notFound } from "next/navigation"; -import { VariablePage } from "./VariablePage"; +import { loadVariableRevisionFull } from "@/variables/data/fullVariableRevision"; +import { loadVariableCard } from "@/variables/data/variableCards"; -import QueryNode, { - VariablePageQuery, -} from "@/__generated__/VariablePageQuery.graphql"; +import { VariableRevisionPage } from "./revisions/[revisionId]/VariableRevisionPage"; type Props = { params: Promise<{ owner: string; slug: string; variableName: string }>; @@ -12,17 +11,32 @@ type Props = { export default async function OuterVariablePage({ params }: Props) { const { owner, slug, variableName } = await params; - const query = await loadPageQuery(QueryNode, { - input: { - owner, - slug, - variableName, - }, + + const variable = await loadVariableCard({ + owner, + slug, + variableName, }); + if (!variable?.currentRevision) { + // "currentRevision" check won't happen, layout won't render the page if it's null + notFound(); + } + + const revision = await loadVariableRevisionFull({ + owner, + slug, + variableName, + revisionId: variable.currentRevision.id, + }); + + if (!revision) { + notFound(); + } + return (
- +
); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/SquiggleVariableRevisionPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx similarity index 70% rename from packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/SquiggleVariableRevisionPage.tsx rename to packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx index 7578488d32..ad2b319e45 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/SquiggleVariableRevisionPage.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx @@ -1,6 +1,5 @@ "use client"; import { FC, use } from "react"; -import { graphql, useFragment } from "react-relay"; import { useAdjustSquiggleVersion, @@ -9,9 +8,8 @@ import { versionSupportsSqPathV2, } from "@quri/versioned-squiggle-components"; -import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; - -import { SquiggleVariableRevisionPage$key } from "@/__generated__/SquiggleVariableRevisionPage.graphql"; +import { sqProjectWithHubLinker } from "@/squiggle/linker"; +import { VariableRevisionFullDTO } from "@/variables/data/fullVariableRevision"; type SquiggleProps = { variableName: string; @@ -51,27 +49,17 @@ const VersionedSquiggleVariableRevisionPage: FC< ); }; -export const SquiggleVariableRevisionPage: FC<{ - variableName: string; - contentRef: SquiggleVariableRevisionPage$key; -}> = ({ variableName, contentRef }) => { - const content = useFragment( - graphql` - fragment SquiggleVariableRevisionPage on SquiggleSnippet { - id - code - version - } - `, - contentRef - ); +export const VariableRevisionPage: FC<{ + revision: VariableRevisionFullDTO; +}> = ({ revision }) => { + const content = revision.modelRevision.squiggleSnippet; const checkedVersion = useAdjustSquiggleVersion(content.version); if (!versionSupportsExports.plain(checkedVersion)) { return (
- Export view pages don't support Squiggle {checkedVersion}. + Variable view pages don't support Squiggle {checkedVersion}.
); } @@ -79,7 +67,7 @@ export const SquiggleVariableRevisionPage: FC<{ return ( ); diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx new file mode 100644 index 0000000000..e31419374e --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx @@ -0,0 +1,31 @@ +import { notFound } from "next/navigation"; + +import { loadVariableRevisionFull } from "@/variables/data/fullVariableRevision"; + +import { VariableRevisionPage } from "./VariableRevisionPage"; + +export default async function OuterVariableRevisionPage({ + params, +}: { + params: Promise<{ + revisionId: string; + owner: string; + slug: string; + variableName: string; + }>; +}) { + const { revisionId, owner, slug, variableName } = await params; + + const revision = await loadVariableRevisionFull({ + owner, + slug, + variableName, + revisionId, + }); + + if (!revision) { + notFound(); + } + + return ; +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/ViewModelPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/ViewModelPage.tsx deleted file mode 100644 index 12d41fe61c..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/view/ViewModelPage.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql } from "react-relay"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { ViewSquiggleSnippet } from "@/squiggle/components/ViewSquiggleSnippet"; - -import { ViewModelPageQuery } from "@/__generated__/ViewModelPageQuery.graphql"; - -export const ViewModelPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ model: result }] = usePageQuery( - graphql` - query ViewModelPageQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - id - slug - currentRevision { - content { - ...ViewSquiggleSnippet - __typename - } - } - } - } - } - `, - query - ); - - const model = extractFromGraphqlErrorUnion(result, "Model"); - - const content = model.currentRevision.content; - const typename = content.__typename; - - switch (typename) { - case "SquiggleSnippet": - return ; - default: - return
Unknown model type {typename}
; - } -}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx new file mode 100644 index 0000000000..efddd32a03 --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx @@ -0,0 +1,26 @@ +"use client"; +import { FC, use, useMemo } from "react"; + +import { + useAdjustSquiggleVersion, + versionedSquigglePackages, +} from "@quri/versioned-squiggle-components"; + +import { ModelCardDTO } from "@/models/data/cards"; +import { sqProjectWithHubLinker } from "@/squiggle/linker"; + +type Props = { + data: NonNullable; +}; + +export const ViewSquiggleSnippet: FC = ({ data }) => { + const { version, code } = data; + + const checkedVersion = useAdjustSquiggleVersion(version); + + const squiggle = use(versionedSquigglePackages(checkedVersion)); + + const project = useMemo(() => sqProjectWithHubLinker(squiggle), [squiggle]); + + return ; +}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/layout.tsx new file mode 100644 index 0000000000..3054ff2a9e --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/view/layout.tsx @@ -0,0 +1,5 @@ +import { PropsWithChildren } from "react"; + +export default function Layout({ children }: PropsWithChildren) { + return
{children}
; +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx index 5ea3b4bbfa..d62790678f 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx @@ -1,24 +1,31 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { notFound } from "next/navigation"; -import { ViewModelPage } from "./ViewModelPage"; - -import QueryNode, { - ViewModelPageQuery, -} from "@/__generated__/ViewModelPageQuery.graphql"; +import { ViewSquiggleSnippet } from "@/app/models/[owner]/[slug]/view/ViewSquiggleSnippet"; +import { loadModelCard } from "@/models/data/cards"; type Props = { params: Promise<{ owner: string; slug: string }>; }; -export default async function OuterModelPage({ params }: Props) { +// Note: this page is currently unused, we used it a long time ago. +// Views are now per-variable, not per-model. +export default async function ViewModelPage({ params }: Props) { const { owner, slug } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, - }); + const model = await loadModelCard({ owner, slug }); + if (!model) { + notFound(); + } + const currentRevision = model.currentRevision; + + switch (currentRevision.contentType) { + case "SquiggleSnippet": { + if (!currentRevision.squiggleSnippet) { + throw new Error("No squiggle snippet"); + } - return ( -
- -
- ); + return ; + } + default: + return
Unknown model type {currentRevision.contentType}
; + } } diff --git a/packages/hub/src/app/new/definition/NewDefinition.tsx b/packages/hub/src/app/new/definition/NewDefinition.tsx index 81bba3e2f8..317588966a 100644 --- a/packages/hub/src/app/new/definition/NewDefinition.tsx +++ b/packages/hub/src/app/new/definition/NewDefinition.tsx @@ -1,80 +1,43 @@ "use client"; -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { FC } from "react"; -import { graphql } from "relay-runtime"; import { H1 } from "@/components/ui/Headers"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; +import { relativeValuesRoute } from "@/lib/routes"; +import { createRelativeValuesDefinitionAction } from "@/relative-values/actions/createRelativeValuesDefinitionAction"; import { RelativeValuesDefinitionForm } from "@/relative-values/components/RelativeValuesDefinitionForm"; -import { FormShape } from "@/relative-values/components/RelativeValuesDefinitionForm/FormShape"; -import { relativeValuesRoute } from "@/routes"; - -import { NewDefinitionMutation } from "@/__generated__/NewDefinitionMutation.graphql"; - -const Mutation = graphql` - mutation NewDefinitionMutation( - $input: MutationCreateRelativeValuesDefinitionInput! - ) { - result: createRelativeValuesDefinition(input: $input) { - __typename - ... on ValidationError { - message - } - ... on CreateRelativeValuesDefinitionResult { - definition { - id - slug - owner { - slug - } - } - } - } - } -`; export const NewDefinition: FC = () => { - useSession({ required: true }); - const router = useRouter(); - const [runMutation] = useAsyncMutation({ - mutation: Mutation, - expectedTypename: "CreateRelativeValuesDefinitionResult", - confirmation: "Definition created", - blockOnSuccess: true, - }); - - const save = async (data: FormShape) => { - await runMutation({ - variables: { - input: { + return ( +
+

New Relative Values definition

+ ({ slug: data.slug, title: data.title, - items: data.items, - clusters: data.clusters, - recommendedUnit: data.recommendedUnit, - }, - }, - onCompleted: (result) => { - if (result.__typename === "CreateRelativeValuesDefinitionResult") { + items: data.items.map((item) => ({ + ...item, + clusterId: item.clusterId ?? undefined, + })), + clusters: data.clusters.map((cluster) => ({ + ...cluster, + recommendedUnit: cluster.recommendedUnit ?? undefined, + })), + recommendedUnit: data.recommendedUnit ?? undefined, + })} + onSuccess={(data) => { router.push( relativeValuesRoute({ - owner: result.definition.owner.slug, - slug: result.definition.slug, + owner: data.owner, + slug: data.slug, }) ); - } - }, - }); - }; - - return ( -
-

New Relative Values definition

- + }} + />
); }; diff --git a/packages/hub/src/app/new/definition/page.tsx b/packages/hub/src/app/new/definition/page.tsx index 1b3a4d656b..0f87fef358 100644 --- a/packages/hub/src/app/new/definition/page.tsx +++ b/packages/hub/src/app/new/definition/page.tsx @@ -1,14 +1,17 @@ import { Metadata } from "next"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { WithAuth } from "@/components/WithAuth"; import { NewDefinition } from "./NewDefinition"; export default function OuterNewModelDefinitionPage() { return ( - - - + + + + + ); } diff --git a/packages/hub/src/app/new/group/NewGroup.tsx b/packages/hub/src/app/new/group/NewGroup.tsx index 7fc9541443..592d5b3bbe 100644 --- a/packages/hub/src/app/new/group/NewGroup.tsx +++ b/packages/hub/src/app/new/group/NewGroup.tsx @@ -1,62 +1,36 @@ "use client"; -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { FC } from "react"; import { FormProvider } from "react-hook-form"; -import { graphql } from "relay-runtime"; import { Button } from "@quri/ui"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { useMutationForm } from "@/hooks/useMutationForm"; -import { groupRoute } from "@/routes"; - -import { NewGroupMutation } from "@/__generated__/NewGroupMutation.graphql"; - -const Mutation = graphql` - mutation NewGroupMutation($input: MutationCreateGroupInput!) { - result: createGroup(input: $input) { - __typename - ... on BaseError { - message - } - ... on CreateGroupResult { - group { - id - slug - } - } - } - } -`; +import { createGroupAction } from "@/groups/actions/createGroupAction"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; +import { groupRoute } from "@/lib/routes"; export const NewGroup: FC = () => { - useSession({ required: true }); - const router = useRouter(); type FormShape = { slug: string | undefined; }; - const { form, onSubmit, inFlight } = useMutationForm< + const { form, onSubmit, inFlight } = useSafeActionForm< FormShape, - NewGroupMutation, - "CreateGroupResult" + typeof createGroupAction >({ defaultValues: {}, mode: "onChange", - mutation: Mutation, - expectedTypename: "CreateGroupResult", blockOnSuccess: true, - formDataToVariables: (data) => ({ - input: { - slug: data.slug ?? "", // shouldn't happen, but satisfies TypeScript - }, + formDataToInput: (data) => ({ + slug: data.slug ?? "", // shouldn't happen, but satisfies TypeScript }), - onCompleted(result) { - router.push(groupRoute({ slug: result.group.slug })); + action: createGroupAction, + onSuccess(result) { + router.push(groupRoute({ slug: result.slug })); }, }); diff --git a/packages/hub/src/app/new/group/page.tsx b/packages/hub/src/app/new/group/page.tsx index 0d33e59c35..3035067b83 100644 --- a/packages/hub/src/app/new/group/page.tsx +++ b/packages/hub/src/app/new/group/page.tsx @@ -1,14 +1,17 @@ import { Metadata } from "next"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { WithAuth } from "@/components/WithAuth"; import { NewGroup } from "./NewGroup"; export default function OuterNewGroupPage() { return ( - - - + + + + + ); } diff --git a/packages/hub/src/app/new/model/NewModel.tsx b/packages/hub/src/app/new/model/NewModel.tsx index ec22897eba..f181e9f9ae 100644 --- a/packages/hub/src/app/new/model/NewModel.tsx +++ b/packages/hub/src/app/new/model/NewModel.tsx @@ -1,30 +1,15 @@ "use client"; -import { useSession } from "next-auth/react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { FC, useEffect } from "react"; -import { FormProvider } from "react-hook-form"; -import { useLazyLoadQuery } from "react-relay"; -import { graphql } from "relay-runtime"; +import { useAction } from "next-safe-action/hooks"; +import { useRouter } from "next/navigation"; +import { FC, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; -import { generateSeed } from "@quri/squiggle-lang"; -import { Button, CheckboxFormField } from "@quri/ui"; -import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; +import { Button, CheckboxFormField, useToast } from "@quri/ui"; import { SelectGroup, SelectGroupOption } from "@/components/SelectGroup"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { useMutationForm } from "@/hooks/useMutationForm"; -import { modelRoute, newModelRoute } from "@/routes"; - -import { NewModelMutation } from "@/__generated__/NewModelMutation.graphql"; -import { NewModelPageQuery } from "@/__generated__/NewModelPageQuery.graphql"; - -const defaultCode = `/* -Describe your code here -*/ - -a = normal(2, 5) -`; +import { createModelAction } from "@/models/actions/createModelAction"; type FormShape = { slug: string | undefined; @@ -32,88 +17,54 @@ type FormShape = { isPrivate: boolean; }; -export const NewModel: FC = () => { - useSession({ required: true }); +export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ + initialGroup, +}) => { + const [group] = useState(initialGroup); - const searchParams = useSearchParams(); + const toast = useToast(); + const router = useRouter(); - const { group: initialGroup } = useLazyLoadQuery( - graphql` - query NewModelPageQuery($groupSlug: String!, $groupSlugIsSet: Boolean!) { - group(slug: $groupSlug) @include(if: $groupSlugIsSet) { - ... on Group { - id - slug - myMembership { - id - } - } - } + const { executeAsync, isPending } = useAction(createModelAction, { + onSuccess: ({ data }) => { + if (data) { + // redirect in action is incompatible with https://github.com/TheEdoRan/next-safe-action/issues/303 + // (and might a bad idea anyway, returning an url is more verbose but more flexible for reuse) + router.push(data.url); + } + }, + onError: ({ error }) => { + if (error.serverError) { + toast(error.serverError, "error"); + return; } - `, - { - groupSlug: searchParams.get("group") ?? "", - groupSlugIsSet: Boolean(searchParams.get("group")), - } - ); - const router = useRouter(); - useEffect(() => { - router.replace(newModelRoute()); // clean up group=... param - }, [router]); + const slugError = error.validationErrors?.slug?._errors?.[0]; + if (slugError) { + form.setError("slug", { + message: slugError, + }); + } else { + toast("Internal error", "error"); + } + }, + }); - const { form, onSubmit, inFlight } = useMutationForm< - FormShape, - NewModelMutation, - "CreateSquiggleSnippetModelResult" - >({ + const form = useForm({ mode: "onChange", defaultValues: { // don't pass `slug: ""` here, it will lead to form reset if a user started to type in a value before JS finished loading - group: initialGroup?.myMembership ? initialGroup : null, + group, isPrivate: false, }, - mutation: graphql` - mutation NewModelMutation( - $input: MutationCreateSquiggleSnippetModelInput! - ) { - result: createSquiggleSnippetModel(input: $input) { - __typename - ... on BaseError { - message - } - ... on CreateSquiggleSnippetModelResult { - model { - id - slug - owner { - slug - } - } - } - } - } - `, - expectedTypename: "CreateSquiggleSnippetModelResult", - blockOnSuccess: true, - formDataToVariables: (data) => ({ - input: { - slug: data.slug ?? "", // shouldn't happen but satisfies Typescript - groupSlug: data.group?.slug, - isPrivate: data.isPrivate, - code: defaultCode, - version: defaultSquiggleVersion, - seed: generateSeed(), - }, - }), - onCompleted: (result) => { - router.push( - modelRoute({ - owner: result.model.owner.slug, - slug: result.model.slug, - }) - ); - }, + }); + + const onSubmit = form.handleSubmit(async (data) => { + await executeAsync({ + slug: data.slug ?? "", // shouldn't happen but satisfies Typescript + groupSlug: data.group?.slug, + isPrivate: data.isPrivate, + }); }); return ( @@ -138,7 +89,7 @@ export const NewModel: FC = () => {
diff --git a/packages/hub/src/app/settings/choose-username/page.tsx b/packages/hub/src/app/settings/choose-username/page.tsx index ac66b8e38c..54359fa4a4 100644 --- a/packages/hub/src/app/settings/choose-username/page.tsx +++ b/packages/hub/src/app/settings/choose-username/page.tsx @@ -1,8 +1,16 @@ import { Metadata } from "next"; +import { redirect } from "next/navigation"; + +import { getSessionUserOrRedirect } from "@/users/auth"; import { ChooseUsername } from "./ChooseUsername"; -export default function OuterChooseUsernamePage() { +export default async function OuterChooseUsernamePage() { + const sessionUser = await getSessionUserOrRedirect(); + if (sessionUser.username) { + redirect("/"); + } + return ; } diff --git a/packages/hub/src/app/status/StatusPage.tsx b/packages/hub/src/app/status/StatusPage.tsx deleted file mode 100644 index 3a37d1005d..0000000000 --- a/packages/hub/src/app/status/StatusPage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { H1 } from "@/components/ui/Headers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { StatusPageQuery } from "@/__generated__/StatusPageQuery.graphql"; - -const Query = graphql` - query StatusPageQuery { - globalStatistics { - users - models - relativeValuesDefinitions - } - } -`; - -const StatRow: FC<{ name: string; value: number }> = ({ name, value }) => ( - - {name} - {value} - -); - -export const StatusPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ globalStatistics: stats }] = usePageQuery(Query, query); - - return ( -
-

Global statistics

- - - - - - -
-
- ); -}; diff --git a/packages/hub/src/app/status/layout.tsx b/packages/hub/src/app/status/layout.tsx new file mode 100644 index 0000000000..fc9d59e792 --- /dev/null +++ b/packages/hub/src/app/status/layout.tsx @@ -0,0 +1,15 @@ +import { PropsWithChildren } from "react"; + +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { H1 } from "@/components/ui/Headers"; + +export default function StatusLayout({ children }: PropsWithChildren) { + return ( + +
+

Global statistics

+ {children} +
+
+ ); +} diff --git a/packages/hub/src/app/status/page.tsx b/packages/hub/src/app/status/page.tsx index 1742dbc63c..adb6381216 100644 --- a/packages/hub/src/app/status/page.tsx +++ b/packages/hub/src/app/status/page.tsx @@ -1,24 +1,34 @@ import { Metadata } from "next"; +import { FC } from "react"; -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { getGlobalStatistics } from "@/lib/server/globalStatistics"; -import { StatusPage } from "./StatusPage"; - -import QueryNode, { - StatusPageQuery, -} from "@/__generated__/StatusPageQuery.graphql"; +const StatRow: FC<{ name: string; value: number }> = ({ name, value }) => ( + + {name} + {value} + +); export default async function OuterFrontPage() { - const query = await loadPageQuery(QueryNode, {}); + const stats = await getGlobalStatistics(); return ( - - - + + + + + + +
); } export const metadata: Metadata = { title: "Status", }; + +export const dynamic = "force-dynamic"; diff --git a/packages/hub/src/app/users/[username]/NewModelButton.tsx b/packages/hub/src/app/users/[username]/NewModelButton.tsx new file mode 100644 index 0000000000..94efc5d467 --- /dev/null +++ b/packages/hub/src/app/users/[username]/NewModelButton.tsx @@ -0,0 +1,33 @@ +"use client"; +import { useRouter, useSelectedLayoutSegment } from "next/navigation"; +import { FC } from "react"; + +import { Button, PlusIcon } from "@quri/ui"; + +import { newDefinitionRoute, newGroupRoute, newModelRoute } from "@/lib/routes"; + +export const NewModelButton: FC = () => { + const segment = useSelectedLayoutSegment(); + + let link = newModelRoute(); + let text = "New Model"; + + if (segment === "groups") { + link = newGroupRoute(); + text = "New Group"; + } else if (segment === "definitions") { + link = newDefinitionRoute(); + text = "New Definition"; + } + + const router = useRouter(); + + return ( + + ); +}; diff --git a/packages/hub/src/app/users/[username]/UserLayout.tsx b/packages/hub/src/app/users/[username]/UserLayout.tsx deleted file mode 100644 index d110d0babb..0000000000 --- a/packages/hub/src/app/users/[username]/UserLayout.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client"; -import { useRouter, useSelectedLayoutSegment } from "next/navigation"; -import { FC, PropsWithChildren } from "react"; -import { graphql } from "relay-runtime"; - -import { Button, PlusIcon, UserIcon } from "@quri/ui"; - -import { H1 } from "@/components/ui/Headers"; -import { StyledTabLink } from "@/components/ui/StyledTabLink"; -import { useUsername } from "@/hooks/useUsername"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { - newDefinitionRoute, - newGroupRoute, - newModelRoute, - userDefinitionsRoute, - userGroupsRoute, - userRoute, - userVariablesRoute, -} from "@/routes"; - -import { UserLayoutQuery } from "@/__generated__/UserLayoutQuery.graphql"; - -const Query = graphql` - query UserLayoutQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on User { - username - # fields for count (empty/non-empty) - # TODO: implement "totalCount" field instead - models(first: 1) { - edges { - __typename - } - } - variables(first: 1) { - edges { - __typename - } - } - relativeValuesDefinitions(first: 1) { - edges { - __typename - } - } - groups(first: 1) { - edges { - __typename - } - } - } - } - } -`; - -const NewButton: FC = () => { - const segment = useSelectedLayoutSegment(); - - let link = newModelRoute(); - let text = "New Model"; - - if (segment === "groups") { - link = newGroupRoute(); - text = "New Group"; - } else if (segment === "definitions") { - link = newDefinitionRoute(); - text = "New Definition"; - } - - const router = useRouter(); - - return ( - - ); -}; - -export const UserLayout: FC< - PropsWithChildren<{ - query: SerializablePreloadedQuery; - }> -> = ({ query, children }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - const myUsername = useUsername(); - const isMe = user.username === myUsername; - - return ( -
-

-
- - {user.username} -
-

-
- - {isMe || user.models.edges.length ? ( - - ) : null} - {isMe || user.variables.edges.length ? ( - - ) : null} - {isMe || user.relativeValuesDefinitions.edges.length ? ( - - ) : null} - {isMe || user.groups.edges.length ? ( - - ) : null} - - {isMe && } -
-
{children}
-
- ); -}; diff --git a/packages/hub/src/app/users/[username]/UserModelList.tsx b/packages/hub/src/app/users/[username]/UserModelList.tsx deleted file mode 100644 index 8740e2f3db..0000000000 --- a/packages/hub/src/app/users/[username]/UserModelList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { ModelList } from "@/models/components/ModelList"; - -import { UserModelList$key } from "@/__generated__/UserModelList.graphql"; - -const Fragment = graphql` - fragment UserModelList on User - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "UserModelListPaginationQuery") { - models(first: $count, after: $cursor) - @connection(key: "UserModelList_models") { - edges { - __typename - } - ...ModelList - } - } -`; - -type Props = { - dataRef: UserModelList$key; -}; - -export const UserModelList: FC = ({ dataRef }) => { - const { - data: { models }, - loadNext, - } = usePaginationFragment(Fragment, dataRef); - - return ( -
- {models.edges.length ? ( - - ) : ( -
No models to show.
- )} -
- ); -}; diff --git a/packages/hub/src/app/users/[username]/UserPage.tsx b/packages/hub/src/app/users/[username]/UserPage.tsx deleted file mode 100644 index 420094c54a..0000000000 --- a/packages/hub/src/app/users/[username]/UserPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { UserModelList } from "./UserModelList"; - -import { UserPageQuery } from "@/__generated__/UserPageQuery.graphql"; - -const Query = graphql` - query UserPageQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on User { - ...UserModelList - } - } - } -`; - -export const UserPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - return ; -}; diff --git a/packages/hub/src/app/users/[username]/definitions/UserDefinitionList.tsx b/packages/hub/src/app/users/[username]/definitions/UserDefinitionList.tsx deleted file mode 100644 index a72bbf894b..0000000000 --- a/packages/hub/src/app/users/[username]/definitions/UserDefinitionList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; - -import { UserDefinitionList$key } from "@/__generated__/UserDefinitionList.graphql"; - -const Fragment = graphql` - fragment UserDefinitionList on User - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "UserDefinitionListPaginationQuery") { - relativeValuesDefinitions(first: $count, after: $cursor) - @connection(key: "UserViewList_relativeValuesDefinitions") { - edges { - __typename - } - ...RelativeValuesDefinitionList - } - } -`; - -type Props = { - dataRef: UserDefinitionList$key; -}; - -export const UserDefinitionList: FC = ({ dataRef }) => { - const { - data: { relativeValuesDefinitions }, - loadNext, - } = usePaginationFragment(Fragment, dataRef); - - return ( -
- {relativeValuesDefinitions.edges.length ? ( - - ) : ( -
No definitions to show.
- )} -
- ); -}; diff --git a/packages/hub/src/app/users/[username]/definitions/UserDefinitionsPage.tsx b/packages/hub/src/app/users/[username]/definitions/UserDefinitionsPage.tsx deleted file mode 100644 index 2332e7acab..0000000000 --- a/packages/hub/src/app/users/[username]/definitions/UserDefinitionsPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { UserDefinitionList } from "./UserDefinitionList"; - -import { UserDefinitionsPageQuery } from "@/__generated__/UserDefinitionsPageQuery.graphql"; - -const Query = graphql` - query UserDefinitionsPageQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on User { - ...UserDefinitionList - } - } - } -`; - -export const UserDefinitionsPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - return ; -}; diff --git a/packages/hub/src/app/users/[username]/definitions/page.tsx b/packages/hub/src/app/users/[username]/definitions/page.tsx index 372d767814..f7747175ad 100644 --- a/packages/hub/src/app/users/[username]/definitions/page.tsx +++ b/packages/hub/src/app/users/[username]/definitions/page.tsx @@ -1,24 +1,19 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { UserDefinitionsPage } from "./UserDefinitionsPage"; - -import QueryNode, { - UserDefinitionsPageQuery, -} from "@/__generated__/UserDefinitionsPageQuery.graphql"; +import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; +import { loadDefinitionCards } from "@/relative-values/data/cards"; type Props = { params: Promise<{ username: string }>; }; -export default async function OuterUserDefinitionsPage({ params }: Props) { +export default async function UserDefinitionsPage({ params }: Props) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { + const page = await loadDefinitionCards({ username, }); - return ; + return ; } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/app/users/[username]/groups/UserGroupList.tsx b/packages/hub/src/app/users/[username]/groups/UserGroupList.tsx deleted file mode 100644 index c1a96d19d6..0000000000 --- a/packages/hub/src/app/users/[username]/groups/UserGroupList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { GroupList } from "@/groups/components/GroupList"; - -import { UserGroupList$key } from "@/__generated__/UserGroupList.graphql"; - -const Fragment = graphql` - fragment UserGroupList on User - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "UserGroupListPaginationQuery") { - groups(first: $count, after: $cursor) - @connection(key: "UserGroupList_groups") { - edges { - __typename - } - ...GroupList - } - } -`; - -type Props = { - dataRef: UserGroupList$key; -}; - -export const UserGroupList: FC = ({ dataRef }) => { - const { - data: { groups }, - loadNext, - } = usePaginationFragment(Fragment, dataRef); - - return ( -
- {groups.edges.length ? ( - - ) : ( -
No groups to show.
- )} -
- ); -}; diff --git a/packages/hub/src/app/users/[username]/groups/UserGroupsPage.tsx b/packages/hub/src/app/users/[username]/groups/UserGroupsPage.tsx deleted file mode 100644 index 999b8f8c39..0000000000 --- a/packages/hub/src/app/users/[username]/groups/UserGroupsPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { UserGroupList } from "./UserGroupList"; - -import { UserGroupsPageQuery } from "@/__generated__/UserGroupsPageQuery.graphql"; - -const Query = graphql` - query UserGroupsPageQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on User { - ...UserGroupList - } - } - } -`; - -export const UserGroupsPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - return ; -}; diff --git a/packages/hub/src/app/users/[username]/groups/page.tsx b/packages/hub/src/app/users/[username]/groups/page.tsx index 7246da8a27..3125cdc281 100644 --- a/packages/hub/src/app/users/[username]/groups/page.tsx +++ b/packages/hub/src/app/users/[username]/groups/page.tsx @@ -1,12 +1,7 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { UserGroupsPage } from "./UserGroupsPage"; - -import QueryNode, { - UserGroupsPageQuery, -} from "@/__generated__/UserGroupsPageQuery.graphql"; +import { GroupList } from "@/groups/components/GroupList"; +import { loadGroupCards } from "@/groups/data/groupCards"; type Props = { params: Promise<{ username: string }>; @@ -14,11 +9,17 @@ type Props = { export default async function OuterUserGroupsPage({ params }: Props) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { - username, - }); + const page = await loadGroupCards({ username }); - return ; + return ( +
+ {page.items.length ? ( + + ) : ( +
No groups to show.
+ )} +
+ ); } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/app/users/[username]/layout.tsx b/packages/hub/src/app/users/[username]/layout.tsx index 657eb9dbf4..2b7e4af270 100644 --- a/packages/hub/src/app/users/[username]/layout.tsx +++ b/packages/hub/src/app/users/[username]/layout.tsx @@ -1,14 +1,25 @@ import { Metadata } from "next"; +import { notFound } from "next/navigation"; import { PropsWithChildren } from "react"; -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { UserIcon } from "@quri/ui"; -import { UserLayout } from "./UserLayout"; +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { H1 } from "@/components/ui/Headers"; +import { + StyledTabLink, + StyledTabLinkList, +} from "@/components/ui/StyledTabLink"; +import { + userDefinitionsRoute, + userGroupsRoute, + userRoute, + userVariablesRoute, +} from "@/lib/routes"; +import { auth } from "@/lib/server/auth"; +import { loadLayoutUser } from "@/users/data/layoutUser"; -import QueryNode, { - UserLayoutQuery, -} from "@/__generated__/UserLayoutQuery.graphql"; +import { NewModelButton } from "./NewModelButton"; type Props = { params: Promise<{ username: string }>; @@ -19,13 +30,53 @@ export default async function OuterUserLayout({ children, }: PropsWithChildren) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { - username, - }); + const user = await loadLayoutUser(username); + if (!user) { + notFound(); + } + const session = await auth(); + const isMe = user.id === session?.user.id; return ( - {children} +
+

+
+ + {user.username} +
+

+
+ + {isMe || user.hasModels ? ( + + ) : null} + {isMe || user.hasVariables ? ( + + ) : null} + {isMe || user.hasDefinitions ? ( + + ) : null} + {isMe || user.hasGroups ? ( + + ) : null} + + {isMe && } +
+
{children}
+
); } diff --git a/packages/hub/src/app/users/[username]/page.tsx b/packages/hub/src/app/users/[username]/page.tsx index c3c5f53180..3367968786 100644 --- a/packages/hub/src/app/users/[username]/page.tsx +++ b/packages/hub/src/app/users/[username]/page.tsx @@ -1,24 +1,27 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { UserPage } from "./UserPage"; - -import QueryNode, { - UserPageQuery, -} from "@/__generated__/UserPageQuery.graphql"; +import { ModelList } from "@/models/components/ModelList"; +import { loadModelCards } from "@/models/data/cards"; type Props = { params: Promise<{ username: string }>; }; -export default async function OuterUserPage({ params }: Props) { +export default async function UserPage({ params }: Props) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { - username, + const page = await loadModelCards({ + ownerSlug: username, }); - return ; + return ( +
+ {page.items.length ? ( + + ) : ( +
No models to show.
+ )} +
+ ); } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/app/users/[username]/variables/UserVariableList.tsx b/packages/hub/src/app/users/[username]/variables/UserVariableList.tsx deleted file mode 100644 index 07f622ae1f..0000000000 --- a/packages/hub/src/app/users/[username]/variables/UserVariableList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { VariableList } from "@/variables/components/VariableList"; - -import { UserVariableList$key } from "@/__generated__/UserVariableList.graphql"; - -const Fragment = graphql` - fragment UserVariableList on User - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "UserVariableListPaginationQuery") { - variables(first: $count, after: $cursor) - @connection(key: "UserVariableList_variables") { - edges { - __typename - } - ...VariableList - } - } -`; - -type Props = { - dataRef: UserVariableList$key; -}; - -export const UserVariableList: FC = ({ dataRef }) => { - const { - data: { variables }, - loadNext, - } = usePaginationFragment(Fragment, dataRef); - - return ( -
- {variables.edges.length ? ( - - ) : ( -
No modelExport to show.
- )} -
- ); -}; diff --git a/packages/hub/src/app/users/[username]/variables/UserVariablesPage.tsx b/packages/hub/src/app/users/[username]/variables/UserVariablesPage.tsx deleted file mode 100644 index 4a04fb386b..0000000000 --- a/packages/hub/src/app/users/[username]/variables/UserVariablesPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { UserVariableList } from "./UserVariableList"; - -import { UserVariablesPageQuery } from "@/__generated__/UserVariablesPageQuery.graphql"; - -const Query = graphql` - query UserVariablesPageQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on User { - ...UserVariableList - } - } - } -`; - -export const UserVariablesPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - return ; -}; diff --git a/packages/hub/src/app/users/[username]/variables/page.tsx b/packages/hub/src/app/users/[username]/variables/page.tsx index 005bd8c015..6217609a63 100644 --- a/packages/hub/src/app/users/[username]/variables/page.tsx +++ b/packages/hub/src/app/users/[username]/variables/page.tsx @@ -1,12 +1,7 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { UserVariablesPage } from "./UserVariablesPage"; - -import QueryNode, { - UserVariablesPageQuery, -} from "@/__generated__/UserVariablesPageQuery.graphql"; +import { VariableList } from "@/variables/components/VariableList"; +import { loadVariableCards } from "@/variables/data/variableCards"; type Props = { params: Promise<{ username: string }>; @@ -14,11 +9,9 @@ type Props = { export default async function OuterUserVariablesPage({ params }: Props) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { - username, - }); + const variables = await loadVariableCards({ ownerSlug: username }); - return ; + return ; } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/components/EntityLayout.tsx b/packages/hub/src/components/EntityLayout.tsx index dc5e6404b7..b94ccd0e69 100644 --- a/packages/hub/src/components/EntityLayout.tsx +++ b/packages/hub/src/components/EntityLayout.tsx @@ -1,8 +1,7 @@ -"use client"; import { clsx } from "clsx"; import { FC, ReactNode } from "react"; -import { EntityNode } from "./EntityInfo"; +import { type EntityNode } from "./EntityInfo"; export type { EntityNode }; diff --git a/packages/hub/src/components/GlobalSearch/SearchResult.tsx b/packages/hub/src/components/GlobalSearch/SearchResult.tsx index cb868623bd..a85f096280 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResult.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResult.tsx @@ -1,7 +1,11 @@ import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { components, type OptionProps } from "react-select"; +import { + SearchResultItem, + TypedSearchResultItem, +} from "@/app/api/search/schema"; + import { Link } from "../ui/Link"; import { SearchOption } from "./"; import { SearchResultGroup } from "./SearchResultGroup"; @@ -9,65 +13,36 @@ import { SearchResultModel } from "./SearchResultModel"; import { SearchResultRelativeValuesDefinition } from "./SearchResultRelativeValuesDefinition"; import { SearchResultUser } from "./SearchResultUser"; -import { SearchResult$key } from "@/__generated__/SearchResult.graphql"; -import { SearchResultEdge$key } from "@/__generated__/SearchResultEdge.graphql"; - -export function useEdgeFragment(edgeFragment: SearchResultEdge$key) { - return useFragment( - graphql` - fragment SearchResultEdge on SearchEdge { - slugSnippet - textSnippet - } - `, - edgeFragment - ); -} - -export type SearchResultComponent = FC<{ - fragment: T; - edgeFragment: SearchResultEdge$key; +export type SearchResultComponent< + T extends SearchResultItem["object"]["type"], +> = FC<{ + item: TypedSearchResultItem; }>; const OkSearchResult: FC<{ - fragment: SearchResult$key; - edgeFragment: SearchResultEdge$key; -}> = ({ fragment: objectRef, edgeFragment }) => { - const object = useFragment( - graphql` - fragment SearchResult on SearchableObject { - __typename - ...SearchResultModel - ...SearchResultRelativeValuesDefinition - ...SearchResultUser - ...SearchResultGroup - } - `, - objectRef - ); - - switch (object.__typename) { + item: SearchResultItem; +}> = ({ item }) => { + switch (item.object.type) { case "Model": return ( - + } /> ); case "RelativeValuesDefinition": return ( } /> ); case "User": - return ; + return } />; case "Group": return ( - + } /> ); default: return (
- Unknown result type: {object.__typename} + Unknown result type: {item.object satisfies never}
); } @@ -78,14 +53,11 @@ export const SearchResult: FC> = ({ ...props }) => { switch (props.data.type) { - case "object": + case "ok": return ( - - + + ); diff --git a/packages/hub/src/components/GlobalSearch/SearchResultGroup.tsx b/packages/hub/src/components/GlobalSearch/SearchResultGroup.tsx index 4d24453cba..e342d45b4a 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResultGroup.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResultGroup.tsx @@ -1,31 +1,13 @@ -import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; - -import { SearchResultGroup$key } from "@/__generated__/SearchResultGroup.graphql"; +import { SearchResultComponent } from "./SearchResult"; import { SearchResultBox } from "./SearchResultBox"; -import { SearchResultComponent, useEdgeFragment } from "./SearchResult"; -import { Snippet } from "./Snippet"; import { SearchResultTitle } from "./SearchResultTItle"; +import { Snippet } from "./Snippet"; -export const SearchResultGroup: SearchResultComponent< - SearchResultGroup$key -> = ({ fragment, edgeFragment }) => { - const edge = useEdgeFragment(edgeFragment); - - // Unused, because `SearchEdge.slugSnippet` is better than `Group.slug`. - useFragment( - graphql` - fragment SearchResultGroup on Group { - slug - } - `, - fragment - ); - +export const SearchResultGroup: SearchResultComponent<"Group"> = ({ item }) => { return ( - {edge.slugSnippet} + {item.slugSnippet} ); diff --git a/packages/hub/src/components/GlobalSearch/SearchResultModel.tsx b/packages/hub/src/components/GlobalSearch/SearchResultModel.tsx index 685afd2d1b..36ff3202e1 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResultModel.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResultModel.tsx @@ -1,34 +1,18 @@ -import { graphql, useFragment } from "react-relay"; - -import { SearchResultModel$key } from "@/__generated__/SearchResultModel.graphql"; +import { SearchResultComponent } from "./SearchResult"; import { SearchResultBox } from "./SearchResultBox"; -import { SearchResultComponent, useEdgeFragment } from "./SearchResult"; +import { SearchResultTitle } from "./SearchResultTItle"; import { Snippet } from "./Snippet"; import { TextSnippet } from "./TextSnippet"; -import { SearchResultTitle } from "./SearchResultTItle"; -export const SearchResultModel: SearchResultComponent< - SearchResultModel$key -> = ({ fragment, edgeFragment }) => { - const edge = useEdgeFragment(edgeFragment); - const model = useFragment( - graphql` - fragment SearchResultModel on Model { - slug - owner { - slug - } - } - `, - fragment - ); +export const SearchResultModel: SearchResultComponent<"Model"> = ({ item }) => { + const model = item.object; return ( - {model.owner.slug}/{edge.slugSnippet} + {model.owner}/{item.slugSnippet} - {edge.textSnippet} + {item.textSnippet} ); }; diff --git a/packages/hub/src/components/GlobalSearch/SearchResultRelativeValuesDefinition.tsx b/packages/hub/src/components/GlobalSearch/SearchResultRelativeValuesDefinition.tsx index 9fb18b5128..050a623d85 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResultRelativeValuesDefinition.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResultRelativeValuesDefinition.tsx @@ -1,30 +1,17 @@ -import { graphql, useFragment } from "react-relay"; - -import { SearchResultRelativeValuesDefinition$key } from "@/__generated__/SearchResultRelativeValuesDefinition.graphql"; -import { SearchResultBox } from "./SearchResultBox"; import { SearchResultComponent } from "./SearchResult"; -import { Snippet } from "./Snippet"; +import { SearchResultBox } from "./SearchResultBox"; import { SearchResultTitle } from "./SearchResultTItle"; +import { Snippet } from "./Snippet"; export const SearchResultRelativeValuesDefinition: SearchResultComponent< - SearchResultRelativeValuesDefinition$key -> = ({ fragment }) => { - const definition = useFragment( - graphql` - fragment SearchResultRelativeValuesDefinition on RelativeValuesDefinition { - slug - owner { - slug - } - } - `, - fragment - ); + "RelativeValuesDefinition" +> = ({ item }) => { + const definition = item.object; return ( - {definition.owner.slug}/{definition.slug} + {definition.owner}/{definition.slug} ); diff --git a/packages/hub/src/components/GlobalSearch/SearchResultUser.tsx b/packages/hub/src/components/GlobalSearch/SearchResultUser.tsx index 9217e71a62..0017475030 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResultUser.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResultUser.tsx @@ -1,31 +1,13 @@ -import { graphql, useFragment } from "react-relay"; - -import { SearchResultUser$key } from "@/__generated__/SearchResultUser.graphql"; +import { SearchResultComponent } from "./SearchResult"; import { SearchResultBox } from "./SearchResultBox"; -import { SearchResultComponent, useEdgeFragment } from "./SearchResult"; -import { Snippet } from "./Snippet"; import { SearchResultTitle } from "./SearchResultTItle"; +import { Snippet } from "./Snippet"; -export const SearchResultUser: SearchResultComponent = ({ - fragment, - edgeFragment, -}) => { - const edge = useEdgeFragment(edgeFragment); - - // Unused, because `SearchEdge.slugSnippet` is better than `User.username`. - useFragment( - graphql` - fragment SearchResultUser on User { - username - } - `, - fragment - ); - +export const SearchResultUser: SearchResultComponent<"User"> = ({ item }) => { return ( - {edge.slugSnippet} + {item.slugSnippet} ); diff --git a/packages/hub/src/components/GlobalSearch/index.tsx b/packages/hub/src/components/GlobalSearch/index.tsx index 235df570c9..297f0f81e9 100644 --- a/packages/hub/src/components/GlobalSearch/index.tsx +++ b/packages/hub/src/components/GlobalSearch/index.tsx @@ -2,7 +2,6 @@ import { clsx } from "clsx"; import { useRouter } from "next/navigation"; import { FC, useRef } from "react"; -import { fetchQuery, graphql, useRelayEnvironment } from "react-relay"; import { components, DropdownIndicatorProps, @@ -12,43 +11,14 @@ import AsyncSelect from "react-select/async"; import { SearchIcon, useGlobalShortcut } from "@quri/ui"; -import { SearchResult } from "./SearchResult"; - -import { GlobalSearchQuery } from "@/__generated__/GlobalSearchQuery.graphql"; -import { SearchResult$key } from "@/__generated__/SearchResult.graphql"; -import { SearchResultEdge$key } from "@/__generated__/SearchResultEdge.graphql"; +import { SearchResultItem, searchResultSchema } from "@/app/api/search/schema"; -export const Query = graphql` - query GlobalSearchQuery($text: String!) { - result: search(text: $text) { - __typename - ... on BaseError { - message - } - ... on QuerySearchConnection { - edges { - cursor - ...SearchResultEdge - node { - id - link - object { - ...SearchResult - } - } - } - } - } - } -`; +import { SearchResult } from "./SearchResult"; export type SearchOption = | { - type: "object"; - id: string; - link: string; - edge: SearchResultEdge$key; - object: SearchResult$key; + type: "ok"; + item: SearchResultItem; } | { type: "error"; @@ -64,36 +34,27 @@ const DropdownIndicator = (props: DropdownIndicatorProps) => { }; export const GlobalSearch: FC = () => { - const environment = useRelayEnvironment(); const router = useRouter(); const loadOptions = async (text: string): Promise => { - const result = await fetchQuery(environment, Query, { - text, - }).toPromise(); - if (!result) return []; - if (result.result.__typename === "BaseError") { - return [ - { - type: "error", - message: result.result.message, - }, - ]; - } else if (result.result.__typename !== "QuerySearchConnection") { + try { + const result = await fetch( + `/api/search?${new URLSearchParams({ query: text })}` + ).then((r) => r.json()); + + const parsed = searchResultSchema.parse(result); + return parsed.map((item) => ({ + type: "ok", + item, + })); + } catch (e) { return [ { type: "error", - message: "Unknown error", + message: String(e), }, ]; } - return result.result.edges.map((edge) => ({ - type: "object", - id: edge.node.id, - link: edge.node.link, - edge, - object: edge.node.object, - })); }; // https://github.com/JedWatson/react-select/discussions/4669#discussioncomment-1994888 @@ -124,17 +85,17 @@ export const GlobalSearch: FC = () => { openMenuOnClick={false} placeholder="Search..." getOptionValue={(option) => - option.type === "error" ? "error" : option.id + option.type === "error" ? "error" : option.item.id } getOptionLabel={(option) => - option.type === "error" ? "error" : option.id + option.type === "error" ? "error" : option.item.id } onKeyDown={(event) => { event.key === "Escape" && ref.current?.blur(); }} onChange={(option) => { - if (option?.type === "object") { - router.push(option.link); + if (option?.type === "ok") { + router.push(option.item.link); } }} controlShouldRenderValue={false} diff --git a/packages/hub/src/components/GroupLink.tsx b/packages/hub/src/components/GroupLink.tsx index a26ca12200..d447185b68 100644 --- a/packages/hub/src/components/GroupLink.tsx +++ b/packages/hub/src/components/GroupLink.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import { StyledLink } from "@/components/ui/StyledLink"; -import { groupRoute } from "@/routes"; +import { groupRoute } from "@/lib/routes"; export const GroupLink: FC<{ slug: string }> = ({ slug }) => { return {slug}; diff --git a/packages/hub/src/components/LoadMoreViaSearchParam.tsx b/packages/hub/src/components/LoadMoreViaSearchParam.tsx index ef9423c8e6..f00d7e71cf 100644 --- a/packages/hub/src/components/LoadMoreViaSearchParam.tsx +++ b/packages/hub/src/components/LoadMoreViaSearchParam.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; -import { useUpdateSearchParams } from "@/hooks/useUpdateSearchParams"; +import { useUpdateSearchParams } from "@/lib/hooks/useUpdateSearchParams"; import { LoadMore } from "./LoadMore"; diff --git a/packages/hub/src/components/OwnerLink.tsx b/packages/hub/src/components/OwnerLink.tsx deleted file mode 100644 index e0eac5a623..0000000000 --- a/packages/hub/src/components/OwnerLink.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { GroupLink } from "./GroupLink"; -import { UsernameLink } from "./UsernameLink"; - -import { OwnerLinkFragment$key } from "@/__generated__/OwnerLinkFragment.graphql"; - -export const OwnerLinkFragment = graphql` - fragment OwnerLinkFragment on Owner { - __typename - id - slug - } -`; - -export const OwnerLink: FC<{ ownerRef: OwnerLinkFragment$key }> = ({ - ownerRef, -}) => { - const owner = useFragment(OwnerLinkFragment, ownerRef); - switch (owner.__typename) { - case "User": - return ; - case "Group": - return ; - default: - throw new Error(`Unknown owner type ${owner.__typename}`); - } -}; diff --git a/packages/hub/src/components/ReactRoot.tsx b/packages/hub/src/components/ReactRoot.tsx index 6349357d53..f40fbfbd85 100644 --- a/packages/hub/src/components/ReactRoot.tsx +++ b/packages/hub/src/components/ReactRoot.tsx @@ -1,33 +1,34 @@ "use client"; -import { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; import { FC, PropsWithChildren } from "react"; -import { RelayEnvironmentProvider } from "react-relay"; import { WithToasts } from "@quri/ui"; import { ErrorBoundary } from "@/components/ErrorBoundary"; -import { getCurrentEnvironment } from "@/relay/environment"; import { ExitConfirmationWrapper } from "./ExitConfirmationWrapper"; +type Props = PropsWithChildren<{ + // CodeMirror tooltips are not compatible with ConfirmationWrapper, because they use a separate React root. + // In a separate root, `useRouter` is not available, because it depends on a global Next.js context. + // TODO: reimplement CodeMirror editor with portals. + confirmationWrapper?: boolean; +}>; + // This component is used in the app's root layout to configure all common providers and wrappers. // It's also useful when you want to mount a separate React root. One example is CodeMirror tooltips, which are mounted as separate DOM elements. -export const ReactRoot: FC> = ({ - session, +export const ReactRoot: FC = ({ children, + confirmationWrapper = true, }) => { - const environment = getCurrentEnvironment(); - - return ( - - - - - {children} - - - - + let content = ( + + {children} + ); + + if (confirmationWrapper) { + content = {content}; + } + + return content; }; diff --git a/packages/hub/src/components/SelectGroup.tsx b/packages/hub/src/components/SelectGroup.tsx index b630bfd2bc..0bc8cec181 100644 --- a/packages/hub/src/components/SelectGroup.tsx +++ b/packages/hub/src/components/SelectGroup.tsx @@ -1,30 +1,13 @@ "use client"; import { FieldPathByValue, FieldValues } from "react-hook-form"; -import { useRelayEnvironment } from "react-relay"; -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SelectFormField } from "@quri/ui"; -import { - SelectGroupQuery, - SelectGroupQuery$data, -} from "@/__generated__/SelectGroupQuery.graphql"; - -const Query = graphql` - query SelectGroupQuery($input: GroupsQueryInput!) { - groups(input: $input) { - edges { - node { - id - slug - } - } - } - } -`; - -export type SelectGroupOption = - SelectGroupQuery$data["groups"]["edges"][number]["node"]; +export type SelectGroupOption = { + id: string; + slug: string; +}; export function SelectGroup< TValues extends FieldValues, @@ -45,23 +28,30 @@ export function SelectGroup< required?: boolean; myOnly?: boolean; }) { - const environment = useRelayEnvironment(); - const loadOptions = async ( inputValue: string ): Promise => { - const result = await fetchQuery(environment, Query, { - input: { - slugContains: inputValue, - myOnly, - }, - }).toPromise(); - - if (!result) { + if (typeof window === "undefined") { return []; } - return result.groups.edges.map((edge) => edge.node); + const result = await fetch( + `/api/find-owners?${new URLSearchParams({ + search: inputValue, + mode: myOnly ? "my-groups" : "all-groups", + })}` + ).then((r) => r.json()); + + const data = z + .array( + z.object({ + id: z.string(), + slug: z.string(), + }) + ) + .parse(result); + + return data; }; return ( diff --git a/packages/hub/src/components/SelectOwner.tsx b/packages/hub/src/components/SelectOwner.tsx index 3d27c81045..964e76e6f2 100644 --- a/packages/hub/src/components/SelectOwner.tsx +++ b/packages/hub/src/components/SelectOwner.tsx @@ -1,47 +1,12 @@ "use client"; import { FC } from "react"; import { FieldPathByValue, FieldValues } from "react-hook-form"; -import { useRelayEnvironment } from "react-relay"; -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SelectFormField } from "@quri/ui"; import { ownerIcon } from "@/lib/ownerIcon"; -import { SelectOwnerQuery } from "@/__generated__/SelectOwnerQuery.graphql"; - -const Query = graphql` - query SelectOwnerQuery($search: String!, $myOnly: Boolean!) { - me @include(if: $myOnly) { - asUser { - __typename - id - slug - } - } - users(input: { usernameContains: $search }) @skip(if: $myOnly) { - edges { - node { - __typename - id - slug - } - } - } - groups(input: { slugContains: $search, myOnly: $myOnly }) { - edges { - node { - __typename - id - slug - } - } - } - } -`; - -// Note: we can't wrap this in a fragment because it's not possible to call `useFragment` -// in SelectFormField callbacks. export type SelectOwnerOption = { __typename: "User" | "Group"; id: string; @@ -75,29 +40,27 @@ export function SelectOwner< required?: boolean; myOnly?: boolean; }) { - const environment = useRelayEnvironment(); - const loadOptions = async ( inputValue: string ): Promise => { - const result = await fetchQuery(environment, Query, { - search: inputValue, - myOnly, - }).toPromise(); + const result = await fetch( + `/api/find-owners?${new URLSearchParams({ + search: inputValue, + mode: myOnly ? "my" : "all", + })}` + ).then((r) => r.json()); - if (!result) { - return []; - } + const data = z + .array( + z.object({ + __typename: z.enum(["User", "Group"]), + id: z.string(), + slug: z.string(), + }) + ) + .parse(result); - const options: SelectOwnerOption[] = []; - if (result.me) { - options.push(result.me.asUser); - } - if (result.users) { - options.push(...(result.users.edges.map((edge) => edge.node) ?? [])); - } - options.push(...result.groups.edges.map((edge) => edge.node)); - return options; + return data; }; return ( diff --git a/packages/hub/src/components/SelectUser.tsx b/packages/hub/src/components/SelectUser.tsx index 3e20e64a8f..c22a6da359 100644 --- a/packages/hub/src/components/SelectUser.tsx +++ b/packages/hub/src/components/SelectUser.tsx @@ -1,30 +1,13 @@ "use client"; import { FieldPathByValue, FieldValues } from "react-hook-form"; -import { useRelayEnvironment } from "react-relay"; -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SelectFormField } from "@quri/ui"; -import { - SelectUserQuery, - SelectUserQuery$data, -} from "@/__generated__/SelectUserQuery.graphql"; - -const Query = graphql` - query SelectUserQuery($input: UsersQueryInput!) { - users(input: $input) { - edges { - node { - id - username - } - } - } - } -`; - -export type SelectUserOption = - SelectUserQuery$data["users"]["edges"][number]["node"]; +export type SelectUserOption = { + id: string; + slug: string; +}; export function SelectUser({ name, @@ -35,22 +18,26 @@ export function SelectUser({ label?: string; required?: boolean; }) { - const environment = useRelayEnvironment(); - const loadOptions = async ( inputValue: string ): Promise => { - const result = await fetchQuery(environment, Query, { - input: { - usernameContains: inputValue, - }, - }).toPromise(); - - if (!result) { - return []; - } - - return result.users.edges.map((edge) => edge.node); + const result = await fetch( + `/api/find-owners?${new URLSearchParams({ + search: inputValue, + mode: "all-users", + })}` + ).then((r) => r.json()); + + const data = z + .array( + z.object({ + id: z.string(), + slug: z.string(), + }) + ) + .parse(result); + + return data; }; return ( @@ -60,7 +47,7 @@ export function SelectUser({ required={required} async loadOptions={loadOptions} - renderOption={(user) => user.username} + renderOption={(user) => user.slug} /> ); } diff --git a/packages/hub/src/components/UsernameLink.tsx b/packages/hub/src/components/UsernameLink.tsx index df8a3e76dc..ea40eb3a01 100644 --- a/packages/hub/src/components/UsernameLink.tsx +++ b/packages/hub/src/components/UsernameLink.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import { StyledLink } from "@/components/ui/StyledLink"; -import { userRoute } from "@/routes"; +import { userRoute } from "@/lib/routes"; export const UsernameLink: FC<{ username: string }> = ({ username }) => { return {username}; diff --git a/packages/hub/src/components/WithAuth/RedirectToLogin.tsx b/packages/hub/src/components/WithAuth/RedirectToLogin.tsx new file mode 100644 index 0000000000..2a48e6e386 --- /dev/null +++ b/packages/hub/src/components/WithAuth/RedirectToLogin.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { redirect, usePathname } from "next/navigation"; +import { FC } from "react"; + +export const RedirectToLogin: FC = () => { + const pathname = usePathname(); + redirect(`/api/auth/signin?callbackUrl=${pathname}`); +}; diff --git a/packages/hub/src/components/WithAuth/index.tsx b/packages/hub/src/components/WithAuth/index.tsx new file mode 100644 index 0000000000..ec5fd6a16b --- /dev/null +++ b/packages/hub/src/components/WithAuth/index.tsx @@ -0,0 +1,33 @@ +import { FC, PropsWithChildren } from "react"; + +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { isRootEmail, isSignedIn } from "@/users/auth"; + +import { RedirectToLogin } from "./RedirectToLogin"; + +type Props = PropsWithChildren<{ + rootOnly?: boolean; +}>; + +export const WithAuth: FC = async ({ children, rootOnly = false }) => { + const session = await auth(); + if (!isSignedIn(session)) { + return ; + } + + if (rootOnly) { + const user = await prisma.user.findUniqueOrThrow({ + where: { email: session.user.email }, + }); + if (!(user.email && user.emailVerified && isRootEmail(user.email))) { + return ( +
+
Unauthorized
+
+ ); + } + } + + return <>{children}; +}; diff --git a/packages/hub/src/components/exports/EditRelativeValueExports.tsx b/packages/hub/src/components/exports/EditRelativeValueExports.tsx index a914c18290..0bfaaa664c 100644 --- a/packages/hub/src/components/exports/EditRelativeValueExports.tsx +++ b/packages/hub/src/components/exports/EditRelativeValueExports.tsx @@ -1,27 +1,22 @@ -"use client"; import { FC, useState } from "react"; import { useForm } from "react-hook-form"; -import { graphql, useFragment } from "react-relay"; import { Button, TextFormField } from "@quri/ui"; +import { RelativeValuesExportInput } from "@/app/models/[owner]/[slug]/EditSquiggleSnippetModel"; import { modelForRelativeValuesExportRoute, relativeValuesRoute, -} from "@/routes"; +} from "@/lib/routes"; +import { ModelFullDTO } from "@/models/data/full"; +import { FindRelativeValuesForSelectResult } from "@/relative-values/data/findRelativeValuesForSelect"; import { SelectOwner, SelectOwnerOption } from "../SelectOwner"; import { FormModal } from "../ui/FormModal"; import { H2 } from "../ui/Headers"; import { StyledDefinitionLink } from "../ui/StyledDefinitionLink"; import { StyledLink } from "../ui/StyledLink"; -import { - SelectRelativeValuesDefinition, - SelectRelativeValuesDefinitionOption, -} from "./SelectRelativeValuesDefinition"; - -import { EditRelativeValueExports_Model$key } from "@/__generated__/EditRelativeValueExports_Model.graphql"; -import { RelativeValuesExportInput } from "@/__generated__/EditSquiggleSnippetModelMutation.graphql"; +import { SelectRelativeValuesDefinition } from "./SelectRelativeValuesDefinition"; const CreateVariableWithDefinitionModal: FC<{ close: () => void; @@ -30,7 +25,7 @@ const CreateVariableWithDefinitionModal: FC<{ type FormShape = { variableName: string; owner: SelectOwnerOption | null; - definition: SelectRelativeValuesDefinitionOption | null; + definition: FindRelativeValuesForSelectResult | null; }; type ValidatedFormShape = { @@ -90,22 +85,9 @@ const CreateVariableWithDefinitionModal: FC<{ const ExportItem: FC<{ item: RelativeValuesExportInput; - modelRef: EditRelativeValueExports_Model$key; + model: ModelFullDTO; remove: () => void; -}> = ({ item, modelRef, remove }) => { - const model = useFragment( - graphql` - fragment EditRelativeValueExports_Model on Model { - id - slug - owner { - slug - } - } - `, - modelRef - ); - +}> = ({ item, model, remove }) => { return (
@@ -139,14 +121,14 @@ type Props = { append: (item: RelativeValuesExportInput) => void; remove: (id: number) => void; items: RelativeValuesExportInput[]; - modelRef: EditRelativeValueExports_Model$key; + model: ModelFullDTO; }; export const EditRelativeValueExports: FC = ({ append, remove, items, - modelRef, + model, }) => { const [isOpen, setIsOpen] = useState(false); @@ -158,7 +140,7 @@ export const EditRelativeValueExports: FC = ({ remove(i)} /> ))} diff --git a/packages/hub/src/components/exports/RelativeValuesExportItem.tsx b/packages/hub/src/components/exports/RelativeValuesExportItem.tsx deleted file mode 100644 index c32acd7b80..0000000000 --- a/packages/hub/src/components/exports/RelativeValuesExportItem.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FC, PropsWithChildren } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { relativeValuesRoute } from "@/routes"; - -import { StyledDefinitionLink } from "../ui/StyledDefinitionLink"; - -import { RelativeValuesExportItem$key } from "@/__generated__/RelativeValuesExportItem.graphql"; - -export const RelativeValuesExportItemFragment = graphql` - fragment RelativeValuesExportItem on RelativeValuesExport { - id - variableName - definition { - id - owner { - id - slug - } - slug - } - } -`; - -const Container: FC = ({ children }) => ( -
-
- {children} -
-
-); - -const RawItem: FC = () => Raw view; - -type Props = { - itemRef?: RelativeValuesExportItem$key; -}; - -const NonEmptyItem: FC> = ({ itemRef }) => { - const item = useFragment(RelativeValuesExportItemFragment, itemRef); - - return ( - - {item.variableName} →{" "} - - {item.definition.owner.slug}/{item.definition.slug} - - - ); -}; - -export const RelativeValuesExportItem: FC = ({ itemRef }) => { - return itemRef ? : ; -}; diff --git a/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx b/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx index 8e2cc3646b..7696589155 100644 --- a/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx +++ b/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx @@ -1,36 +1,14 @@ import { FC, useEffect } from "react"; import { FieldPathByValue, FieldValues, useFormContext } from "react-hook-form"; -import { useRelayEnvironment } from "react-relay"; -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SelectFormField } from "@quri/ui"; -import { SelectOwnerOption } from "../SelectOwner"; - -import { - SelectRelativeValuesDefinitionQuery, - SelectRelativeValuesDefinitionQuery$data, -} from "@/__generated__/SelectRelativeValuesDefinitionQuery.graphql"; - -const Query = graphql` - query SelectRelativeValuesDefinitionQuery( - $input: RelativeValuesDefinitionsQueryInput! - ) { - relativeValuesDefinitions(input: $input) { - edges { - node { - id - slug - } - } - } - } -`; +import { FindRelativeValuesForSelectResult } from "@/relative-values/data/findRelativeValuesForSelect"; -export type SelectRelativeValuesDefinitionOption = - SelectRelativeValuesDefinitionQuery$data["relativeValuesDefinitions"]["edges"][number]["node"]; +import { SelectOwnerOption } from "../SelectOwner"; -const DefinitionInfo: FC<{ option: SelectRelativeValuesDefinitionOption }> = ({ +const DefinitionInfo: FC<{ option: FindRelativeValuesForSelectResult }> = ({ option, }) =>
{option.slug}
; @@ -41,36 +19,40 @@ export function SelectRelativeValuesDefinition< label, ownerFieldName, }: { - name: FieldPathByValue; + name: FieldPathByValue; label?: string; ownerFieldName: FieldPathByValue; }) { const { watch, resetField } = useFormContext(); - const environment = useRelayEnvironment(); const owner: SelectOwnerOption | null = watch(ownerFieldName); const loadOptions = async ( inputValue: string - ): Promise => { + ): Promise => { if (!owner) { return []; } - const result = await fetchQuery( - environment, - Query, - { - input: { - owner: owner.slug, - slugContains: inputValue, - }, - } - ).toPromise(); + const result = await fetch( + `/api/find-relative-values?${new URLSearchParams({ + owner: owner.slug, + slugContains: inputValue, + })}` + ).then((r) => r.json()); + + const data = z + .array( + z.object({ + id: z.string(), + slug: z.string(), + }) + ) + .parse(result); if (!result) { return []; } - return result.relativeValuesDefinitions.edges.map((edge) => edge.node); + return result; }; useEffect(() => { @@ -78,7 +60,7 @@ export function SelectRelativeValuesDefinition< }, [name, owner, resetField]); return ( - + key={owner?.slug ?? " "} // re-render the component when owner changes; this helps with default select options on initial open name={name} label={label} diff --git a/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx b/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx index e11ff595e0..f0be0b138b 100644 --- a/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx +++ b/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx @@ -1,15 +1,16 @@ +import { Session } from "next-auth"; import { signIn } from "next-auth/react"; import { FC } from "react"; import { Button, Dropdown, DropdownMenu } from "@quri/ui"; -import { useUsername } from "@/hooks/useUsername"; - import { DropdownWithArrow } from "./DropdownWithArrow"; import { UserControlsMenu } from "./UserControlsMenu"; -export const DesktopUserControls: FC = () => { - const username = useUsername(); +export const DesktopUserControls: FC<{ session: Session | null }> = ({ + session, +}) => { + const username = session?.user?.username; return username ? (
diff --git a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx index 938caae839..0566996f2d 100644 --- a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx @@ -1,46 +1,30 @@ import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { DropdownMenuHeader, GroupIcon, PlusIcon } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; -import { groupRoute, newGroupRoute } from "@/routes"; - -import { MyGroupsMenu$key } from "@/__generated__/MyGroupsMenu.graphql"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { groupRoute, newGroupRoute } from "@/lib/routes"; +import { Paginated } from "@/lib/types"; type Props = { - groupsRef: MyGroupsMenu$key; + groups: Paginated; close: () => void; }; -export const MyGroupsMenu: FC = ({ groupsRef, close }) => { - const groups = useFragment( - graphql` - fragment MyGroupsMenu on Query { - result: groups(input: { myOnly: true }) { - edges { - node { - id - slug - } - } - } - } - `, - groupsRef - ); +export const MyGroupsMenu: FC = ({ groups, close }) => { return ( <> My Groups - {groups.result.edges.length ? ( + {groups.items.length ? ( <> - {groups.result.edges.map((edge) => ( + {groups.items.map((group) => ( ))} @@ -52,7 +36,7 @@ export const MyGroupsMenu: FC = ({ groupsRef, close }) => { title="New Group" close={close} /> - {/* TODO: "...show all" link is hasNextPage is true */} + {/* TODO: "...show all" link is loadNext is true */} ); }; diff --git a/packages/hub/src/components/layout/RootLayout/PageFooter.tsx b/packages/hub/src/components/layout/RootLayout/PageFooter.tsx index 3a804ceb46..984fa28af5 100644 --- a/packages/hub/src/components/layout/RootLayout/PageFooter.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageFooter.tsx @@ -1,8 +1,6 @@ -"use client"; import Image from "next/image"; import { FC } from "react"; import { FaDiscord, FaGithub, FaRss } from "react-icons/fa"; -import { SiGraphql } from "react-icons/si"; import { Link } from "@/components/ui/Link"; import { @@ -10,14 +8,13 @@ import { GITHUB_URL, NEWSLETTER_URL, QURI_DONATE_URL, -} from "@/lib/common"; -import logoPic from "@/public/logo.png"; +} from "@/lib/constants"; import { aboutRoute, - graphqlPlaygroundRoute, privacyPolicyRoute, termsOfServiceRoute, -} from "@/routes"; +} from "@/lib/routes"; +import logoPic from "@/public/logo.png"; const linkClasses = "items-center flex hover:text-gray-900"; @@ -74,10 +71,6 @@ export const PageFooter: FC = () => { Newsletter - - - API -
); diff --git a/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx b/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx new file mode 100644 index 0000000000..cb09e9fbc3 --- /dev/null +++ b/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { FC } from "react"; + +import { isAiRoute, isModelRoute } from "@/lib/routes"; + +import { PageFooter } from "./PageFooter"; + +// This is a client component because it needs to know the current pathname. +// Alternatively, we could use route groups (https://nextjs.org/docs/app/building-your-application/routing/route-groups), +// and then the footer could be RSC. But that would complicate the `app/` tree more than it's worth. +export const PageFooterIfNecessary: FC = () => { + const pathname = usePathname(); + + const showFooter = !isModelRoute(pathname) && !isAiRoute(pathname); + + return showFooter ? : null; +}; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index 25866b5c7a..8a8a82e028 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -1,11 +1,12 @@ -import { signIn, useSession } from "next-auth/react"; +"use client"; +import { Session } from "next-auth"; +import { signIn, signOut } from "next-auth/react"; import { FC, useState } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { BoltIcon, BookOpenIcon, + Button, DotsHorizontalIcon, Dropdown, DropdownMenu, @@ -16,10 +17,10 @@ import { UserCircleIcon, } from "@quri/ui"; -import { Link } from "@/components/ui/Link"; -import { useUsername } from "@/hooks/useUsername"; -import { SQUIGGLE_DOCS_URL } from "@/lib/common"; -import { aboutRoute, aiRoute, newModelRoute } from "@/routes"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { SQUIGGLE_DOCS_URL } from "@/lib/constants"; +import { aboutRoute, aiRoute, newModelRoute } from "@/lib/routes"; +import { Paginated } from "@/lib/types"; import { GlobalSearch } from "../../GlobalSearch"; import { DesktopUserControls } from "./DesktopUserControls"; @@ -29,13 +30,7 @@ import { MenuLinkModeProps, PageMenuLink } from "./PageMenuLink"; import { useForceChooseUsername } from "./useForceChooseUsername"; import { UserControlsMenu } from "./UserControlsMenu"; -import { PageMenu$key } from "@/__generated__/PageMenu.graphql"; - const AboutMenuLink: FC = (props) => { - const { data: session } = useSession(); - if (session) { - return null; - } return ; }; @@ -54,38 +49,27 @@ const AiMenuLink: FC = (props) => ( ); const NewModelMenuLink: FC = (props) => { - const { data: session } = useSession(); - if (!session) { - return null; - } return ( ); }; -const fragment = graphql` - fragment PageMenu on Query - @argumentDefinitions(signedIn: { type: "Boolean!" }) { - ...MyGroupsMenu @include(if: $signedIn) - } -`; - type MenuProps = { - queryRef: PageMenu$key; + groups: Paginated; + session: Session | null; }; -const DesktopMenu: FC = ({ queryRef }) => { - const { data: session } = useSession(); - const menu = useFragment(fragment, queryRef); +const DesktopMenu: FC = ({ groups, session }) => { return (
- + {!session && } {session ? ( <> @@ -93,7 +77,7 @@ const DesktopMenu: FC = ({ queryRef }) => { ( - + )} > @@ -102,15 +86,13 @@ const DesktopMenu: FC = ({ queryRef }) => { ) : null} - +
); }; -const MobileMenu: FC = ({ queryRef }) => { - const menu = useFragment(fragment, queryRef); - - const username = useUsername(); +const MobileMenu: FC = ({ groups, session }) => { + const username = session?.user?.username; const [open, setOpen] = useState(false); const Icon = username ? UserCircleIcon : DotsHorizontalIcon; @@ -136,12 +118,12 @@ const MobileMenu: FC = ({ queryRef }) => {
Menu - - + {session && } + {!session && } {username ? ( <> - + = ({ queryRef }) => { ); }; -export const PageMenu: FC = ({ queryRef }) => { - useForceChooseUsername(); +export const PageMenu: FC = ({ session, groups }) => { + // TODO - if redirecting, return a custom menu; right now we render the + // confused version where "New Model" button is visible, but "Sign In" button + // is visible too + const { shouldChoose } = useForceChooseUsername(session); + + if (shouldChoose) { + return ( + + ); + } return ( -
- - Squiggle Hub - + <>
- +
- +
-
+ ); }; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx b/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx index be25ffea90..a28e4cff8f 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx @@ -21,6 +21,7 @@ type Props = { href: string; icon?: FC; external?: boolean; + prefetch?: boolean; } & MenuLinkModeProps; export const PageMenuLink: FC = ({ @@ -30,6 +31,7 @@ export const PageMenuLink: FC = ({ icon, href, external, + prefetch, }) => { const Icon = icon; return mode === "desktop" ? ( @@ -37,6 +39,7 @@ export const PageMenuLink: FC = ({ className="select-none rounded-md px-2 py-1 text-sm text-white hover:bg-slate-700" href={href} target={external ? "_blank" : undefined} + prefetch={prefetch} > {Icon && } {title} @@ -47,6 +50,7 @@ export const PageMenuLink: FC = ({ icon={icon ?? EmptyIcon} title={title} close={close} + prefetch={prefetch} /> ); }; diff --git a/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx b/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx index 728dfb0398..0497afb145 100644 --- a/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx @@ -10,7 +10,7 @@ import { } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; -import { newDefinitionRoute, userRoute } from "@/routes"; +import { newDefinitionRoute, userRoute } from "@/lib/routes"; type Props = { close: () => void; diff --git a/packages/hub/src/components/layout/RootLayout/index.tsx b/packages/hub/src/components/layout/RootLayout/index.tsx new file mode 100644 index 0000000000..a96ac6a73b --- /dev/null +++ b/packages/hub/src/components/layout/RootLayout/index.tsx @@ -0,0 +1,55 @@ +import { FC, PropsWithChildren, Suspense } from "react"; + +import { Link } from "@/components/ui/Link"; +import { loadGroupCards } from "@/groups/data/groupCards"; +import { auth } from "@/lib/server/auth"; + +import { ReactRoot } from "../../ReactRoot"; +import { PageFooterIfNecessary } from "./PageFooterIfNecessary"; +import { PageMenu } from "./PageMenu"; + +const WrappedPageMenu: FC = async () => { + // TODO - we wait for the session, and then we do another GraphQL query in + // ``, sequentially. We could select all relevant session data + // through GraphQL, or avoid GraphQL queries altogether. + const session = await auth(); + const username = session?.user?.username; + const groups = username + ? await loadGroupCards({ username: session?.user?.username }) + : { items: [] }; + + return ; +}; + +const InnerRootLayout: FC = ({ children }) => { + return ( +
+
+ + Squiggle Hub + + {/* Top menu is not essential for fetching and rendering other content, so we render it in a Suspense boundary */} + + + +
+
+ {children} +
+ +
+ ); +}; + +export const RootLayout: FC = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts index ec90ad14cc..5143532655 100644 --- a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts +++ b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts @@ -1,17 +1,22 @@ -import { useSession } from "next-auth/react"; +import { Session } from "next-auth"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect } from "react"; -import { chooseUsernameRoute } from "@/routes"; +import { chooseUsernameRoute } from "@/lib/routes"; -export function useForceChooseUsername() { - const { data: session } = useSession(); +export function useForceChooseUsername(session: Session | null) { + const pathname = usePathname(); + const router = useRouter(); - if ( - session?.user && - !session?.user.username && - !window.location.href.includes(chooseUsernameRoute()) - ) { - // Next's redirect() is broken for components included from the root layout - // https://github.com/vercel/next.js/issues/42556 (it's closed but not really solved) - window.location.href = chooseUsernameRoute(); - } + const shouldChoose = session?.user && !session.user.username; + const shouldRedirect = + shouldChoose && !pathname.includes(chooseUsernameRoute()); + + useEffect(() => { + if (shouldRedirect) { + router.push(chooseUsernameRoute()); + } + }, [shouldRedirect, router]); + + return { shouldRedirect, shouldChoose }; } diff --git a/packages/hub/src/components/ui/CloseDropdownOnInvariantChange.tsx b/packages/hub/src/components/ui/CloseDropdownOnInvariantChange.tsx new file mode 100644 index 0000000000..28ea1bf2ed --- /dev/null +++ b/packages/hub/src/components/ui/CloseDropdownOnInvariantChange.tsx @@ -0,0 +1,21 @@ +import { FC, useEffect, useState } from "react"; + +import { useCloseDropdown } from "@quri/ui"; + +export const useCloseDropdownOnInvariantChange = (invariant: unknown) => { + const close = useCloseDropdown(); + const [initialInvariant] = useState(invariant); + useEffect(() => { + if (invariant !== initialInvariant) { + close(); + } + }, [invariant, initialInvariant, close]); +}; + +export const CloseDropdownOnInvariantChange: FC<{ + invariant: unknown; +}> = ({ invariant }) => { + useCloseDropdownOnInvariantChange(invariant); + + return null; +}; diff --git a/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx b/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx index 1159677967..84ba1a3c1c 100644 --- a/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx +++ b/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx @@ -9,6 +9,7 @@ type Props = { title: string; icon?: FC; close: () => void; + prefetch?: boolean; }; export const DropdownMenuNextLinkItem: FC = ({ @@ -16,9 +17,10 @@ export const DropdownMenuNextLinkItem: FC = ({ href, icon, close, + prefetch, }) => { return ( - + ); diff --git a/packages/hub/src/components/ui/MutationAction.tsx b/packages/hub/src/components/ui/MutationAction.tsx deleted file mode 100644 index 98991e1c1a..0000000000 --- a/packages/hub/src/components/ui/MutationAction.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { FC, ReactNode } from "react"; -import { UseMutationConfig } from "react-relay"; -import { GraphQLTaggedNode, OperationType, VariablesOf } from "relay-runtime"; - -import { DropdownMenuAsyncActionItem, IconProps } from "@quri/ui"; - -import { - CommonMutationParameters, - useAsyncMutation, -} from "@/hooks/useAsyncMutation"; - -type MaybeLazyVariablesOf = - | VariablesOf - // Note that this is less type-safe than variables object; extra values will be ignored. - // TODO: I think this might be possible to fix with more advanced Typescript. - // We could make this type a generic over function's return type and then compare it against - // actual `VariablesOf` and throw `never` if there are extra keys). - // Until that is fixed, plain `VariablesOf` should be preferred. - | (() => VariablesOf); - -function resolveVariables( - variables: MaybeLazyVariablesOf -): VariablesOf { - if (typeof variables === "function") { - return variables(); - } else { - return variables; - } -} - -export function MutationAction< - TMutation extends CommonMutationParameters = never, - const TTypename extends string = never, ->({ - mutation, - variables, - updater, - expectedTypename, - title, - icon, - close, -}: { - mutation: GraphQLTaggedNode; - variables: MaybeLazyVariablesOf; - updater?: UseMutationConfig["updater"]; - expectedTypename: TTypename; - title: string; - icon?: FC; - close: () => void; -}): ReactNode { - const [runMutation] = useAsyncMutation({ - mutation, - expectedTypename, - }); - - const act = async () => { - await runMutation({ - variables: resolveVariables(variables), - updater, - }); - }; - - return ( - - ); -} diff --git a/packages/hub/src/components/ui/MutationButton.tsx b/packages/hub/src/components/ui/MutationButton.tsx deleted file mode 100644 index aa72c15fb3..0000000000 --- a/packages/hub/src/components/ui/MutationButton.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { ReactNode } from "react"; -import { GraphQLTaggedNode } from "relay-runtime"; - -import { Button } from "@quri/ui"; - -import { - CommonMutationParameters, - useAsyncMutation, - UseAsyncMutationAct, -} from "@/hooks/useAsyncMutation"; -import { useAsync } from "react-select/async"; - -/* - * Props for this component include: - * - some props that are passed to ` - ); -} diff --git a/packages/hub/src/components/ui/MutationModalAction.tsx b/packages/hub/src/components/ui/MutationModalAction.tsx deleted file mode 100644 index ef5282c771..0000000000 --- a/packages/hub/src/components/ui/MutationModalAction.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { FC, PropsWithChildren, ReactNode } from "react"; -import { DefaultValues, FieldPath, FieldValues } from "react-hook-form"; -import { GraphQLTaggedNode, VariablesOf } from "relay-runtime"; - -import { DropdownMenuModalActionItem, IconProps } from "@quri/ui"; - -import { FormModal } from "@/components/ui/FormModal"; -import { CommonMutationParameters } from "@/hooks/useAsyncMutation"; -import { useMutationForm } from "@/hooks/useMutationForm"; - -type CommonProps< - TMutation extends CommonMutationParameters, - TFormShape extends FieldValues, - TTypename extends string, -> = { - mutation: GraphQLTaggedNode; - expectedTypename: TTypename; - formDataToVariables: (data: TFormShape) => VariablesOf; - initialFocus?: FieldPath; - defaultValues?: DefaultValues; - submitText: string; - onCompleted?: ( - data: Extract - ) => void; - close: () => void; -}; - -function MutationFormModal< - TMutation extends CommonMutationParameters, - TFormShape extends FieldValues, - const TTypename extends string, ->({ - mutation, - expectedTypename, - formDataToVariables, - initialFocus, - defaultValues, - submitText, - onCompleted, - close, - title, - children, -}: PropsWithChildren> & { - title: string; -}): ReactNode { - const { form, onSubmit, inFlight } = useMutationForm< - TFormShape, - TMutation, - TTypename - >({ - mode: "onChange", - defaultValues, - mutation, - expectedTypename, - formDataToVariables, - onCompleted(data) { - onCompleted?.(data); - close(); - }, - }); - - return ( - - {children} - - ); -} - -export function MutationModalAction< - TMutation extends CommonMutationParameters, - TFormShape extends FieldValues, - const TTypename extends string = string, ->({ - modalTitle, - title, - icon, - children, - ...modalProps -}: CommonProps & { - modalTitle: string; - title: string; - icon?: FC; - children: () => ReactNode; -}): ReactNode { - return ( - ( - - // Note that we pass the same `close` that's responsible for closing the dropdown. - {...modalProps} - title={modalTitle} - > - {children()} - - )} - /> - ); -} diff --git a/packages/hub/src/components/ui/SafeActionButton.tsx b/packages/hub/src/components/ui/SafeActionButton.tsx new file mode 100644 index 0000000000..309cd5c933 --- /dev/null +++ b/packages/hub/src/components/ui/SafeActionButton.tsx @@ -0,0 +1,57 @@ +"use client"; +import { + InferSafeActionFnInput, + InferSafeActionFnResult, +} from "next-safe-action"; +import { HookSafeActionFn, useAction } from "next-safe-action/hooks"; +import { ReactNode } from "react"; + +import { Button, useToast } from "@quri/ui"; + +export function SafeActionButton< + const Action extends HookSafeActionFn, +>({ + action, + input, + title, + confirmation, + onSuccess, + // button props + theme, + size, +}: { + action: Action; + input: InferSafeActionFnInput["clientInput"]; + onSuccess?: ( + data: NonNullable["data"]> + ) => void; + title: string; + confirmation?: string; +} & Pick[0], "theme" | "size">): ReactNode { + const toast = useToast(); + + const { execute, isPending } = useAction(action, { + onSuccess: ({ data }) => { + if (data) { + if (confirmation) { + toast(confirmation, "confirmation"); + } + onSuccess?.(data); + } + }, + onError: ({ error }) => { + toast( + error.serverError ? String(error.serverError) : "Internal error", + "error" + ); + }, + }); + + return ( + execute(input)}> + + + ); +} diff --git a/packages/hub/src/components/ui/SafeActionDropdownAction.tsx b/packages/hub/src/components/ui/SafeActionDropdownAction.tsx new file mode 100644 index 0000000000..0f2a4ddb1e --- /dev/null +++ b/packages/hub/src/components/ui/SafeActionDropdownAction.tsx @@ -0,0 +1,77 @@ +import { + InferSafeActionFnInput, + InferSafeActionFnResult, +} from "next-safe-action"; +import { HookSafeActionFn, useAction } from "next-safe-action/hooks"; +import { FC } from "react"; + +import { + DropdownMenuActionItem, + IconProps, + useCloseDropdown, + useToast, +} from "@quri/ui"; + +import { useCloseDropdownOnInvariantChange } from "./CloseDropdownOnInvariantChange"; + +export function SafeActionDropdownAction< + const T extends HookSafeActionFn, +>({ + title, + icon, + action, + input, + invariant, + onSuccess, +}: { + title: string; + icon?: FC; + action: T; + input: InferSafeActionFnInput["clientInput"]; + onSuccess?: ( + data: NonNullable["data"]> + ) => void; + // If set, the dropdown will close only after the invariant changes. + // This is useful, because server action returns before it sends back the revalidated UI. + // Re-rendering the new UI might take a while (it's async), so we don't want to close the dropdown immediately. + // This is an ugly workaround; see also: https://github.com/vercel/next.js/discussions/53206 + // Discussion in QURI Slack: https://quri.slack.com/archives/C059EEU0HMM/p1732810277978719 + // + // Also note that in some cases even `invariant` is not enough. Consider the scenario where the list of items in the dropdown is based on the component props. + // In this case, this action would be unmounted before it would get the chance to close the dropdown. + // In that scenario you might prefer to use `` instead. + // (The example of this is ``.) + invariant?: unknown; +}) { + const close = useCloseDropdown(); + const toast = useToast(); + + const { execute, isPending } = useAction(action, { + onSuccess: ({ data }) => { + if (data && invariant === undefined) { + onSuccess?.(data); + // if there's no invariant, close the dropdown immediately + if (invariant === undefined) { + close(); + } + } + }, + onError: ({ error }) => { + toast( + error.serverError ? String(error.serverError) : "Internal error", + "error" + ); + }, + }); + + useCloseDropdownOnInvariantChange(invariant); + + return ( + execute(input)} + /> + ); +} diff --git a/packages/hub/src/components/ui/SafeActionModalAction.tsx b/packages/hub/src/components/ui/SafeActionModalAction.tsx new file mode 100644 index 0000000000..e4fbc726f9 --- /dev/null +++ b/packages/hub/src/components/ui/SafeActionModalAction.tsx @@ -0,0 +1,102 @@ +import { HookSafeActionFn } from "next-safe-action/hooks"; +import { FC, PropsWithChildren, ReactNode } from "react"; +import { FieldPath, FieldValues } from "react-hook-form"; + +import { + DropdownMenuModalActionItem, + IconProps, + useCloseDropdown, +} from "@quri/ui"; + +import { FormModal } from "@/components/ui/FormModal"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; + +type CommonProps< + TFormShape extends FieldValues, + Action extends HookSafeActionFn, +> = Pick< + Parameters>[0], + | "formDataToInput" + | "defaultValues" + | "action" + | "onSuccess" + | "blockOnSuccess" +> & { + initialFocus?: FieldPath; + submitText: string; +}; + +function SafeActionFormModal< + TFormShape extends FieldValues, + const Action extends HookSafeActionFn, +>({ + formDataToInput, + initialFocus, + defaultValues, + submitText, + action, + onSuccess, + title, + children, +}: PropsWithChildren> & { + title: string; +}): ReactNode { + // Note that we use the same `close` that's responsible for closing the dropdown. + const closeDropdown = useCloseDropdown(); + + const { form, onSubmit, inFlight } = useSafeActionForm({ + mode: "onChange", + defaultValues, + action, + formDataToInput, + async onSuccess(data) { + onSuccess?.(data); + closeDropdown(); + }, + }); + + return ( + + {children} + + ); +} + +export function SafeActionModalAction< + TFormShape extends FieldValues, + const Action extends HookSafeActionFn, +>({ + modalTitle, + title, + icon, + children, + ...modalProps +}: CommonProps & { + modalTitle: string; + title: string; + icon?: FC; + children: () => ReactNode; +}): ReactNode { + return ( + ( + + {...modalProps} + title={modalTitle} + > + {children()} + + )} + /> + ); +} diff --git a/packages/hub/src/components/ui/StyledTabLink.tsx b/packages/hub/src/components/ui/StyledTabLink.tsx index bc1bc7eaa0..babeb53f08 100644 --- a/packages/hub/src/components/ui/StyledTabLink.tsx +++ b/packages/hub/src/components/ui/StyledTabLink.tsx @@ -11,6 +11,7 @@ type StyledTabLinkProps = { href: string; icon?: FC; selected?: (pathname: string, href: string) => boolean; + prefetch?: boolean; }; type StyledTabLinkType = React.FC & { @@ -22,15 +23,19 @@ export const StyledTabLink: StyledTabLinkType = ({ href, icon: Icon, selected, + prefetch, }) => { const pathname = usePathname(); const isSelected = selected ? selected(pathname, href) : pathname === href; return ( - + ); }; -StyledTabLink.List = StyledTab.ListDiv; +const StyledTabLinkList = StyledTab.ListDiv; +StyledTabLink.List = StyledTabLinkList; + +export { StyledTabLinkList }; diff --git a/packages/hub/src/constants.ts b/packages/hub/src/constants.ts deleted file mode 100644 index 9c9ca38036..0000000000 --- a/packages/hub/src/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Don't try to destructure this, `const { NEXT_PUBLIC_FOO } = process.env` won't work correctly. - -// Note that only `NEXT_PUBLIC_*` vars are affected; others can be used through `process.env.FOO` without issues. - -export const VERCEL_URL = process.env["NEXT_PUBLIC_VERCEL_BRANCH_URL"]; - -export const SAMPLE_COUNT_DEFAULT = 1000; -export const XY_POINT_LENGTH_DEFAULT = 1000; - -export const DEFAULT_SEED = "DEFAULT_SEED"; diff --git a/packages/hub/src/graphql/builder.ts b/packages/hub/src/graphql/builder.ts deleted file mode 100644 index 78947acdfc..0000000000 --- a/packages/hub/src/graphql/builder.ts +++ /dev/null @@ -1,132 +0,0 @@ -import SchemaBuilder from "@pothos/core"; -import ErrorsPlugin from "@pothos/plugin-errors"; -import PrismaPlugin from "@pothos/plugin-prisma"; -import type PrismaTypes from "@pothos/plugin-prisma/generated"; -import RelayPlugin from "@pothos/plugin-relay"; -import ScopeAuthPlugin from "@pothos/plugin-scope-auth"; -import SimpleObjectsPlugin from "@pothos/plugin-simple-objects"; -import ValidationPlugin from "@pothos/plugin-validation"; -import WithInputPlugin from "@pothos/plugin-with-input"; -import { Session } from "next-auth"; -import { NextRequest } from "next/server"; - -import { prisma } from "@/prisma"; - -import { - getMyMembershipById, - getMyMembershipBySlug, -} from "./helpers/groupHelpers"; -import { isRootEmail } from "./helpers/userHelpers"; - -type Context = { - session: Session | null; - request: NextRequest; -}; - -export type SignedInSession = Session & { - user: NonNullable & { email: string }; -}; - -export type HubSchemaTypes = { - PrismaTypes: PrismaTypes; - DefaultEdgesNullability: false; - Context: Context; - AuthScopes: { - signedIn: boolean; - isRootUser: boolean; - isGroupAdmin: string; - isGroupAdminBySlug: string; - controlsOwnerId: string; - }; - AuthContexts: { - // https://pothos-graphql.dev/docs/plugins/scope-auth#change-context-types-based-on-scopes - signedIn: Context & { - session: SignedInSession; - }; - isRootUser: Context & { - session: SignedInSession; - }; - }; - DefaultAuthStrategy: "all"; -}; - -export const builder = new SchemaBuilder({ - plugins: [ - // this plugin comes before auth plugin; see also: https://github.com/hayes/pothos/issues/464 - ErrorsPlugin, // https://pothos-graphql.dev/docs/plugins/errors - ScopeAuthPlugin, // https://pothos-graphql.dev/docs/plugins/scope-auth - SimpleObjectsPlugin, // https://pothos-graphql.dev/docs/plugins/simple-objects - WithInputPlugin, // https://pothos-graphql.dev/docs/plugins/with-input - PrismaPlugin, // https://pothos-graphql.dev/docs/plugins/prisma - RelayPlugin, // https://pothos-graphql.dev/docs/plugins/relay - ValidationPlugin, // https://pothos-graphql.dev/docs/plugins/validation - ], - prisma: { - client: prisma, - }, - relayOptions: { - clientMutationId: "omit", - cursorType: "String", - edgesFieldOptions: { - nullable: false, - }, - }, - scopeAuthOptions: { - defaultStrategy: "all", - }, - authScopes: async (context) => ({ - signedIn: !!context.session?.user, - isRootUser: () => { - const email = context.session?.user.email; - // Note: there's no emailVerified field in session, is this a problem? Probably not. - // See also: `isRootUser` function in `types/User.ts`. - return !!(email && isRootEmail(email)); - }, - isGroupAdmin: async (groupId) => { - const myMembership = await getMyMembershipById(groupId, context.session); - return myMembership?.role === "Admin"; - }, - isGroupAdminBySlug: async (groupSlug) => { - const myMembership = await getMyMembershipBySlug( - groupSlug, - context.session - ); - return myMembership?.role === "Admin"; - }, - controlsOwnerId: async (ownerId) => { - if (!context.session) { - return false; - } - return Boolean( - await prisma.owner.count({ - where: { - id: ownerId, - OR: [ - { - user: { email: context.session.user.email }, - }, - { - group: { - memberships: { - some: { - user: { - email: context.session.user.email, - }, - }, - }, - }, - }, - ], - }, - }) - ); - }, - }), - errorOptions: { - defaultTypes: [Error], - directResult: true, - }, -}); - -builder.queryType({}); -builder.mutationType({}); diff --git a/packages/hub/src/graphql/errors/BaseError.ts b/packages/hub/src/graphql/errors/BaseError.ts deleted file mode 100644 index ebe7c0315e..0000000000 --- a/packages/hub/src/graphql/errors/BaseError.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { builder } from "@/graphql/builder"; - -import { ErrorInterface } from "./common"; - -// This error is a default, always available when `errors: {}` is enabled. -// See also: https://pothos-graphql.dev/docs/plugins/errors#recommended-usage -builder.objectType(Error, { - name: "BaseError", - interfaces: [ErrorInterface], -}); diff --git a/packages/hub/src/graphql/errors/NotFoundError.ts b/packages/hub/src/graphql/errors/NotFoundError.ts deleted file mode 100644 index 13e6a8160f..0000000000 --- a/packages/hub/src/graphql/errors/NotFoundError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { builder } from "@/graphql/builder"; - -import { ErrorInterface } from "./common"; - -export class NotFoundError extends Error { - constructor(message?: string) { - super(message); - } -} - -builder.objectType(NotFoundError, { - name: "NotFoundError", - interfaces: [ErrorInterface], -}); diff --git a/packages/hub/src/graphql/errors/ValidationError.ts b/packages/hub/src/graphql/errors/ValidationError.ts deleted file mode 100644 index 14fe63140f..0000000000 --- a/packages/hub/src/graphql/errors/ValidationError.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ZodError, ZodIssue } from "zod"; - -import { builder } from "../builder"; -import { ErrorInterface } from "./common"; - -const ValidationErrorIssue = builder - .objectRef("ValidationErrorIssue") - .implement({ - fields: (t) => ({ - message: t.exposeString("message"), - path: t.stringList({ - resolve: (obj) => obj.path.map((item) => String(item)), - }), - }), - }); - -builder.objectType(ZodError, { - name: "ValidationError", - interfaces: [ErrorInterface], - // TODO - patch ZodError.message to be human-readable - fields: (t) => ({ - issues: t.field({ - type: [ValidationErrorIssue], - resolve: (obj) => obj.issues, - }), - message: t.string({ - resolve: (obj) => { - return obj.issues - .map((issue) => `[${issue.path.join(".")}] ${issue.message}`) - .join("\n"); - }, - }), - }), -}); diff --git a/packages/hub/src/graphql/errors/common.ts b/packages/hub/src/graphql/errors/common.ts deleted file mode 100644 index 0c11f5d06a..0000000000 --- a/packages/hub/src/graphql/errors/common.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Prisma } from "@prisma/client"; - -import { builder } from "@/graphql/builder"; - -export const ErrorInterface = builder.interfaceRef("Error").implement({ - fields: (t) => ({ - message: t.exposeString("message"), - }), -}); - -// Rethrows Prisma constraint error (usually happens on create operations) with a nicer error message. -export async function rethrowOnConstraint( - cb: () => Promise, - ...handlers: { - target: string[]; - error: string; - }[] -): Promise { - try { - return await cb(); - } catch (e) { - for (const handler of handlers) { - if ( - e instanceof Prisma.PrismaClientKnownRequestError && - e.code === "P2002" && - Array.isArray(e.meta?.["target"]) && - e.meta?.["target"].join(",") === handler.target.join(",") - ) { - // TODO - throw more specific error - throw new Error(handler.error); - } - } - throw e; - } -} diff --git a/packages/hub/src/graphql/helpers/groupHelpers.ts b/packages/hub/src/graphql/helpers/groupHelpers.ts deleted file mode 100644 index 70a1de298d..0000000000 --- a/packages/hub/src/graphql/helpers/groupHelpers.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Session } from "next-auth"; - -import { prisma } from "@/prisma"; - -import { getSelf, isSignedIn } from "./userHelpers"; - -export async function getMyMembershipById( - groupId: string, - session: Session | null -) { - if (!isSignedIn(session)) { - return null; - } - const self = await getSelf(session); - const myMembership = await prisma.userGroupMembership.findUnique({ - where: { - userId_groupId: { - groupId: groupId, - userId: self.id, - }, - }, - }); - return myMembership; -} - -export async function getMyMembershipBySlug( - groupSlug: string, - session: Session | null -) { - if (!isSignedIn(session)) { - return null; - } - const self = await getSelf(session); - const myMembership = await prisma.userGroupMembership.findFirst({ - where: { - userId: self.id, - group: { - asOwner: { - slug: groupSlug, - }, - }, - }, - }); - return myMembership; -} - -export async function getMembership({ - groupSlug, - userSlug, -}: { - groupSlug: string; - userSlug: string; -}) { - const membership = await prisma.userGroupMembership.findFirst({ - where: { - user: { - asOwner: { - slug: userSlug, - }, - }, - group: { - asOwner: { - slug: groupSlug, - }, - }, - }, - }); - return membership; -} - -export async function getMyMembership({ - groupSlug, - session, -}: { - groupSlug: string; - session: Session | null; -}) { - if (!isSignedIn(session)) { - return null; - } - const self = await getSelf(session); - const myMembership = await prisma.userGroupMembership.findFirst({ - where: { - userId: self.id, - group: { - asOwner: { - slug: groupSlug, - }, - }, - }, - }); - return myMembership; -} - -// also returns true if user is not an admin -export async function groupHasAdminsBesidesUser({ - groupSlug, - userSlug, -}: { - groupSlug: string; - userSlug: string; -}) { - return Boolean( - await prisma.userGroupMembership.count({ - where: { - group: { - asOwner: { - slug: groupSlug, - }, - }, - NOT: { - user: { - asOwner: { - slug: userSlug, - }, - }, - }, - role: "Admin", - }, - }) - ); -} diff --git a/packages/hub/src/graphql/helpers/modelHelpers.ts b/packages/hub/src/graphql/helpers/modelHelpers.ts deleted file mode 100644 index f32d5deab8..0000000000 --- a/packages/hub/src/graphql/helpers/modelHelpers.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Model, Prisma } from "@prisma/client"; -import { Session } from "next-auth"; - -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; - -export function modelWhereHasAccess( - session: Session | null -): Prisma.ModelWhereInput { - const orParts: Prisma.ModelWhereInput[] = [{ isPrivate: false }]; - if (session) { - orParts.push({ - owner: { - OR: [ - { - user: { email: session.user.email }, - }, - { - group: { - memberships: { - some: { - user: { email: session.user.email }, - }, - }, - }, - }, - ], - }, - }); - } - return { OR: orParts }; -} - -export async function getWriteableModel({ - session, - owner, - slug, - include, -}: { - session: Session; - owner: string; - slug: string; - include?: Prisma.ModelInclude; -}): Promise { - // Note: `findUnique` would be safer, but then we won't be able to use nested queries - const model = await prisma.model.findFirst({ - where: { - slug, - owner: { - slug: owner, - OR: [ - { - user: { email: session.user.email }, - }, - { - group: { - memberships: { - some: { - user: { email: session.user.email }, - }, - }, - }, - }, - ], - }, - }, - include, - }); - if (!model) { - // FIXME - this will happen if permissions are not sufficient - // It would be better to throw a custom PermissionError - // (Note that we should throw PermissionError only if model is readable, but not writeable; otherwise it should still be "Can't find") - throw new NotFoundError("Can't find model"); - } - return model; -} diff --git a/packages/hub/src/graphql/helpers/userHelpers.ts b/packages/hub/src/graphql/helpers/userHelpers.ts deleted file mode 100644 index b92d01e977..0000000000 --- a/packages/hub/src/graphql/helpers/userHelpers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { User } from "@prisma/client"; -import { Session } from "next-auth"; - -import { prisma } from "@/prisma"; - -import { SignedInSession } from "../builder"; - -export function isSignedIn( - session: Session | null -): session is SignedInSession { - return Boolean(session?.user.email); -} - -export async function getSelf(session: SignedInSession) { - const user = await prisma.user.findUniqueOrThrow({ - where: { email: session.user.email }, - }); - return user; -} - -const ROOT_EMAILS = (process.env["ROOT_EMAILS"] ?? "").split(","); - -export function isRootEmail(email: string) { - return ROOT_EMAILS.includes(email); -} - -export async function isRootUser(user: User) { - // see also: `isRootUser` auth scope in builder.ts - return Boolean(user.email && user.emailVerified && isRootEmail(user.email)); -} diff --git a/packages/hub/src/graphql/mutations/acceptReusableGroupInviteToken.ts b/packages/hub/src/graphql/mutations/acceptReusableGroupInviteToken.ts deleted file mode 100644 index ce8a0f730c..0000000000 --- a/packages/hub/src/graphql/mutations/acceptReusableGroupInviteToken.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getMyMembership } from "../helpers/groupHelpers"; -import { UserGroupMembership } from "../types/Group"; - -builder.mutationField("acceptReusableGroupInviteToken", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("AcceptReusableGroupInviteTokenResult", { - fields: (t) => ({ - membership: t.field({ type: UserGroupMembership }), - }), - }), - errors: {}, - input: { - groupSlug: t.input.string({ required: true }), - inviteToken: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - let group = await prisma.group.findFirstOrThrow({ - where: { - asOwner: { - slug: input.groupSlug, - }, - }, - }); - - if (group.reusableInviteToken !== input.inviteToken) { - throw new Error("Invalid token"); - } - - const myMembership = await getMyMembership({ - groupSlug: input.groupSlug, - session, - }); - if (myMembership) { - throw new Error("You're already a member of this group"); - } - - const membership = await prisma.userGroupMembership.create({ - data: { - group: { - connect: { - id: group.id, - }, - }, - user: { - connect: { - email: session.user.email, - }, - }, - role: "Member", - }, - }); - - return { membership }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/addUserToGroup.ts b/packages/hub/src/graphql/mutations/addUserToGroup.ts deleted file mode 100644 index 94b7dfb339..0000000000 --- a/packages/hub/src/graphql/mutations/addUserToGroup.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { ZodError } from "zod"; - -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { MembershipRoleType, UserGroupMembership } from "../types/Group"; -import { validateSlug } from "../utils"; - -// Adapted from `inviteUserToGroup`. -builder.mutationField("addUserToGroup", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("AddUserToGroupResult", { - fields: (t) => ({ - membership: t.field({ type: UserGroupMembership }), - }), - }), - errors: { types: [ZodError] }, - input: { - group: t.input.string({ required: true, validate: validateSlug }), - username: t.input.string({ required: true, validate: validateSlug }), - role: t.input.field({ - type: MembershipRoleType, - required: true, - }), - }, - resolve: async (_, { input }, { session }) => { - const membership = await prisma.$transaction(async (tx) => { - const groupOwner = await tx.owner.findUnique({ - where: { - slug: input.group, - }, - }); - if (!groupOwner) { - throw new Error(`Group ${input.group} not found`); - } - - const requestedUser = await tx.user.findFirst({ - where: { - asOwner: { - slug: input.username, - }, - }, - }); - - if (!requestedUser) { - throw new Error(`User ${input.username} not found`); - } - - // We perform all checks one by one because that allows more precise error reporting. - // (It would be possible to check everything in one big query with clever nested `connect` checks.) - const isAdmin = await tx.group.count({ - where: { - ownerId: groupOwner.id, - memberships: { - some: { - user: { email: session.user.email }, - role: "Admin", - }, - }, - }, - }); - if (!isAdmin) { - throw new Error(`You're not an admin of ${input.group} group`); - } - - const alreadyAMember = await tx.group.count({ - where: { - ownerId: groupOwner.id, - memberships: { - some: { userId: requestedUser.id }, - }, - }, - }); - if (alreadyAMember) { - throw new Error( - `${input.username} is already a member of ${input.group}` - ); - } - - // Cancel all pending invites - await tx.groupInvite.updateMany({ - where: { - userId: requestedUser.id, - groupId: groupOwner.id, - status: "Pending", - }, - data: { - status: "Canceled", - }, - }); - - return await tx.userGroupMembership.create({ - data: { - user: { - connect: { id: requestedUser.id }, - }, - group: { - connect: { ownerId: groupOwner.id }, - }, - role: input.role, - }, - }); - }); - - return { membership }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts b/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts deleted file mode 100644 index 39205407ba..0000000000 --- a/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { builder } from "@/graphql/builder"; - -import { rebuildSearchableTable } from "../helpers/searchHelpers"; - -builder.mutationField("adminRebuildSearchIndex", (t) => - t.withAuth({ signedIn: true }).field({ - description: "Admin-only query for rebuilding the search index", - type: builder.simpleObject("AdminRebuildSearchIndexResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - errors: {}, - authScopes: { - isRootUser: true, - }, - resolve: async () => { - await rebuildSearchableTable(); - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/adminUpdateModelVersion.ts b/packages/hub/src/graphql/mutations/adminUpdateModelVersion.ts deleted file mode 100644 index 5b23054488..0000000000 --- a/packages/hub/src/graphql/mutations/adminUpdateModelVersion.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getSelf } from "../helpers/userHelpers"; -import { Model } from "../types/Model"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("adminUpdateModelVersion", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - description: "Admin-only query for upgrading model versions", - type: builder.simpleObject("AdminUpdateModelVersionResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: {}, - authScopes: { - isRootUser: true, - }, - input: { - modelId: t.input.string({ required: true }), - version: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const decodedModelId = decodeGlobalIdWithTypename(input.modelId, "Model"); - const self = await getSelf(session); - - const model = await prisma.$transaction(async (tx) => { - let model = await prisma.model.findUniqueOrThrow({ - where: { id: decodedModelId }, - include: { - currentRevision: { - include: { - squiggleSnippet: true, - relativeValuesExports: true, - }, - }, - }, - }); - if (!model.currentRevision) { - throw new Error("currentRevision is missing"); - } - if ( - model.currentRevision.contentType !== "SquiggleSnippet" || - !model.currentRevision.squiggleSnippet - ) { - throw new Error("Not a Squiggle model"); - } - - const revision = await tx.modelRevision.create({ - data: { - squiggleSnippet: { - create: { - code: model.currentRevision.squiggleSnippet.code, - version: input.version, - seed: model.currentRevision.squiggleSnippet.seed, - }, - }, - contentType: "SquiggleSnippet", - model: { - connect: { id: model.id }, - }, - author: { - connect: { email: self.email! }, - }, - comment: `Automated upgrade from ${model.currentRevision.squiggleSnippet.version} to ${input.version}`, - relativeValuesExports: { - createMany: { - data: model.currentRevision.relativeValuesExports.map( - (exp) => ({ - variableName: exp.variableName, - definitionId: exp.definitionId, - }) - ), - }, - }, - }, - include: { - model: { - select: { - id: true, - }, - }, - }, - }); - - return await tx.model.update({ - where: { - id: revision.model.id, - }, - data: { - currentRevisionId: revision.id, - updatedAt: model.updatedAt, - }, - // TODO - optimize with queryFromInfo, https://pothos-graphql.dev/docs/plugins/prisma#optimized-queries-without-tprismafield - }); - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/buildRelativeValuesCache.ts b/packages/hub/src/graphql/mutations/buildRelativeValuesCache.ts deleted file mode 100644 index a230888970..0000000000 --- a/packages/hub/src/graphql/mutations/buildRelativeValuesCache.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; -import { cartesianProduct } from "@/relative-values/lib/utils"; -import { relativeValuesItemsSchema } from "@/relative-values/types"; -import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; - -import { - getRelativeValuesExportForWriteableModel, - RelativeValuesExport, -} from "../types/RelativeValuesExport"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("buildRelativeValuesCache", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("BuildRelativeValuesCacheResult", { - fields: (t) => ({ - relativeValuesExport: t.field({ type: RelativeValuesExport }), - }), - }), - errors: {}, - input: { - exportId: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const exportId = decodeGlobalIdWithTypename( - input.exportId, - "RelativeValuesExport" - ); - - const relativeValuesExport = - await getRelativeValuesExportForWriteableModel({ - exportId, - session, - }); - - const { modelRevision } = relativeValuesExport; - - if (modelRevision.contentType !== "SquiggleSnippet") { - throw new Error("Unsupported model revision content type"); - } - - const squiggleSnippet = modelRevision.squiggleSnippet; - if (!squiggleSnippet) { - throw new Error("Model content not found"); - } - - const evaluatorResult = await ModelEvaluator.create( - squiggleSnippet.code, - relativeValuesExport.variableName - ); - if (!evaluatorResult.ok) { - throw new Error( - `Failed to create evaluator: ${evaluatorResult.value.toString()}` - ); - } - const evaluator = evaluatorResult.value; - - const definitionRevision = - relativeValuesExport.definition.currentRevision; - if (!definitionRevision) { - throw new Error("Definition revision not found"); - } - - const items = relativeValuesItemsSchema.parse(definitionRevision.items); - const itemIds = items.map((item) => item.id); - - const existingCacheItems = await prisma.relativeValuesPairCache.findMany({ - where: { - exportId, - }, - select: { - firstItem: true, - secondItem: true, - }, - }); - - const seen: Record> = {}; - for (const row of existingCacheItems) { - seen[row.firstItem] ??= {}; - seen[row.firstItem][row.secondItem] = true; - } - - for (const [firstItem, secondItem] of cartesianProduct( - itemIds, - itemIds - )) { - if (seen[firstItem]?.[secondItem]) { - continue; // already cached - } - const result = evaluator.compareWithoutCache(firstItem, secondItem); - await prisma.relativeValuesPairCache.create({ - data: { - exportId, - firstItem, - secondItem, - ...(result.ok - ? { result: result.value } - : { error: result.value.toString() }), - }, - }); - } - - const updatedRelativeValuesExport = - await prisma.relativeValuesExport.findUniqueOrThrow({ - where: { id: exportId }, - }); - return { relativeValuesExport: updatedRelativeValuesExport }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/cancelGroupInvite.ts b/packages/hub/src/graphql/mutations/cancelGroupInvite.ts deleted file mode 100644 index 36839259fc..0000000000 --- a/packages/hub/src/graphql/mutations/cancelGroupInvite.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { GroupInvite } from "../types/GroupInvite"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("cancelGroupInvite", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("CancelGroupInviteResult", { - fields: (t) => ({ - invite: t.field({ type: GroupInvite }), - }), - }), - errors: {}, - input: { - inviteId: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const decodedInviteId = decodeGlobalIdWithTypename(input.inviteId, [ - "UserGroupInvite", - "EmailGroupInvite", - ]); - - const invite = await prisma.groupInvite.update({ - where: { - id: decodedInviteId, - group: { - // permissions check - memberships: { - some: { - user: { email: session.user.email }, - role: "Admin", - }, - }, - }, - }, - data: { - status: "Canceled", - }, - }); - - return { invite }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/clearRelativeValuesCache.ts b/packages/hub/src/graphql/mutations/clearRelativeValuesCache.ts deleted file mode 100644 index e5a7e5f400..0000000000 --- a/packages/hub/src/graphql/mutations/clearRelativeValuesCache.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { - getRelativeValuesExportForWriteableModel, - RelativeValuesExport, -} from "../types/RelativeValuesExport"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("clearRelativeValuesCache", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("ClearRelativeValuesCacheResult", { - fields: (t) => ({ - relativeValuesExport: t.field({ type: RelativeValuesExport }), - }), - }), - errors: {}, - input: { - exportId: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const exportId = decodeGlobalIdWithTypename( - input.exportId, - "RelativeValuesExport" - ); - - await getRelativeValuesExportForWriteableModel({ - exportId, - session, - }); - - await prisma.relativeValuesPairCache.deleteMany({ - where: { exportId }, - }); - - const updatedRelativeValuesExport = - await prisma.relativeValuesExport.findUniqueOrThrow({ - where: { id: exportId }, - }); - return { relativeValuesExport: updatedRelativeValuesExport }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/createGroup.ts b/packages/hub/src/graphql/mutations/createGroup.ts deleted file mode 100644 index 93c8b00d47..0000000000 --- a/packages/hub/src/graphql/mutations/createGroup.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { rethrowOnConstraint } from "../errors/common"; -import { Group } from "../types/Group"; -import { indexGroupId } from "../helpers/searchHelpers"; - -builder.mutationField("createGroup", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("CreateGroupResult", { - fields: (t) => ({ - group: t.field({ type: Group }), - }), - }), - errors: {}, - input: { - slug: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const user = await prisma.user.findUniqueOrThrow({ - where: { email: session.user.email }, - }); - - const group = await rethrowOnConstraint( - () => - prisma.group.create({ - data: { - asOwner: { - create: { - slug: input.slug, - }, - }, - memberships: { - create: [{ userId: user.id, role: "Admin" }], - }, - }, - }), - { - target: ["slug"], - error: `The group ${input.slug} already exists`, - } - ); - - await indexGroupId(group.id); - - return { group }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/createRelativeValuesDefinition.ts b/packages/hub/src/graphql/mutations/createRelativeValuesDefinition.ts deleted file mode 100644 index 94ff2cce6c..0000000000 --- a/packages/hub/src/graphql/mutations/createRelativeValuesDefinition.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { InputObjectRef } from "@pothos/core"; -import { ZodError } from "zod"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { rethrowOnConstraint } from "../errors/common"; -import { getWriteableOwner } from "../helpers/ownerHelpers"; -import { indexDefinitionId } from "../helpers/searchHelpers"; -import { RelativeValuesDefinition } from "../types/RelativeValuesDefinition"; -import { validateSlug } from "../utils"; - -const validateColor = { regex: /^#[0-9a-fA-F]{6}$/ }; - -export const RelativeValuesClusterInput = builder.inputType( - "RelativeValuesClusterInput", - { - fields: (t) => ({ - id: t.string({ - required: true, - validate: validateSlug, - }), - color: t.string({ - required: true, - validate: validateColor, - }), - recommendedUnit: t.string({ - validate: validateSlug, - }), - }), - } -); - -export const RelativeValuesItemInput = builder.inputType( - "RelativeValuesItemInput", - { - fields: (t) => ({ - id: t.string({ - required: true, - validate: validateSlug, - }), - name: t.string({ - required: true, - }), - description: t.string(), - clusterId: t.string(), - }), - } -); - -type ExtractInputShape = Type extends InputObjectRef ? T : never; - -export function validateRelativeValuesDefinition({ - items, - clusters, - recommendedUnit, -}: { - items: ExtractInputShape[]; - clusters: ExtractInputShape[]; - recommendedUnit: string | null | undefined; -}) { - if (!items.length) { - throw new Error("RelativeValuesDefinition must include at least one item"); - } - - const itemIds = new Set(); - for (const item of items) { - if (itemIds.has(item.id)) { - throw new Error(`Duplicate item id ${item.id}`); - } - if (!item.id.match(/^\w[\w\-]*$/)) { - throw new Error(`Invalid item id ${item.id}`); - } - itemIds.add(item.id); - } - - const checkId = (id: string | null | undefined) => { - if (id !== null && id !== undefined && !itemIds.has(id)) { - throw new Error(`id ${id} not found in items`); - } - }; - - for (const cluster of clusters) { - checkId(cluster.recommendedUnit); - } - checkId(recommendedUnit); -} - -builder.mutationField("createRelativeValuesDefinition", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("CreateRelativeValuesDefinitionResult", { - fields: (t) => ({ - definition: t.field({ type: RelativeValuesDefinition }), - }), - }), - errors: { types: [ZodError] }, - input: { - groupSlug: t.input.string({ - validate: validateSlug, - description: - "Optional, if not set, definition will be created on current user's account", - }), - slug: t.input.string({ - required: true, - validate: validateSlug, - }), - title: t.input.string({ required: true }), - items: t.input.field({ - type: [RelativeValuesItemInput], - required: true, - }), - clusters: t.input.field({ - type: [RelativeValuesClusterInput], - required: true, - }), - recommendedUnit: t.input.string({ - validate: validateSlug, - }), - }, - resolve: async (_, { input }, { session }) => { - const owner = await getWriteableOwner(session, input.groupSlug); - - validateRelativeValuesDefinition({ - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - }); - - const definition = await prisma.$transaction(async (tx) => { - const definition = await rethrowOnConstraint( - () => - tx.relativeValuesDefinition.create({ - data: { - ownerId: owner.id, - slug: input.slug, - revisions: { - create: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - }, - }, - }, - }), - { - target: ["slug", "ownerId"], - error: `The definition ${input.slug} already exists on this account`, - } - ); - - const revision = await tx.relativeValuesDefinitionRevision.create({ - data: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - definition: { - connect: { - id: definition.id, - }, - }, - }, - }); - - await tx.relativeValuesDefinition.update({ - where: { - id: definition.id, - }, - data: { - currentRevisionId: revision.id, - }, - }); - - return definition; - }); - - await indexDefinitionId(definition.id); - - return { definition }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/createReusableGroupInviteToken.ts b/packages/hub/src/graphql/mutations/createReusableGroupInviteToken.ts deleted file mode 100644 index 89a3c0a4bf..0000000000 --- a/packages/hub/src/graphql/mutations/createReusableGroupInviteToken.ts +++ /dev/null @@ -1,49 +0,0 @@ -import crypto from "crypto"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { Group } from "../types/Group"; - -builder.mutationField("createReusableGroupInviteToken", (t) => - t.fieldWithInput({ - description: `Create or replace a reusable invite token for a group, available as \`reusableInviteToken\` field on group object. - -You must be an admin of the group to call this mutation. Previous invite token, if it existed, will stop working.`, - type: builder.simpleObject("CreateReusableGroupInviteTokenResult", { - fields: (t) => ({ - group: t.field({ type: Group }), - }), - }), - errors: {}, - authScopes: (_, { input }) => ({ - isGroupAdminBySlug: input.slug, - }), - input: { - slug: t.input.string({ required: true }), - }, - resolve: async (_, { input }) => { - let group = await prisma.group.findFirstOrThrow({ - where: { - asOwner: { - slug: input.slug, - }, - }, - }); - - const token = crypto.randomBytes(30).toString("hex"); - - group = await prisma.group.update({ - where: { - id: group.id, - }, - data: { - // old token will be overwritten, that's fine - reusableInviteToken: token, - }, - }); - - return { group }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts b/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts deleted file mode 100644 index 45381a4709..0000000000 --- a/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ZodError } from "zod"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { rethrowOnConstraint } from "../errors/common"; -import { getWriteableOwner } from "../helpers/ownerHelpers"; -import { indexModelId } from "../helpers/searchHelpers"; -import { getSelf } from "../helpers/userHelpers"; -import { Model } from "../types/Model"; -import { validateSlug } from "../utils"; - -builder.mutationField("createSquiggleSnippetModel", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("CreateSquiggleSnippetModelResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: { types: [ZodError] }, - input: { - groupSlug: t.input.string({ - validate: validateSlug, - description: - "Optional, if not set, model will be created on current user's account", - }), - code: t.input.string({ - required: true, - description: "Squiggle source code", - }), - version: t.input.string({ required: true }), - slug: t.input.string({ - required: true, - validate: validateSlug, - }), - isPrivate: t.input.boolean({ - description: "Defaults to false", - }), - seed: t.input.string({ - required: true, - description: "A unique seed, used for calculation", - }), - }, - resolve: async (_, { input }, { session }) => { - const model = await prisma.$transaction(async (tx) => { - const owner = await getWriteableOwner(session, input.groupSlug); - - // nested create is not possible here; - // similar problem is described here: https://github.com/prisma/prisma/discussions/14937, - // seems to be caused by multiple Model -> ModelRevision relations - const model = await rethrowOnConstraint( - () => - tx.model.create({ - data: { - slug: input.slug, - ownerId: owner.id, - isPrivate: input.isPrivate ?? false, - }, - }), - { - target: ["slug", "ownerId"], - error: `Model ${input.slug} already exists on this account`, - } - ); - - const self = await getSelf(session); - - const revision = await tx.modelRevision.create({ - data: { - squiggleSnippet: { - create: { - code: input.code, - version: input.version, - seed: input.seed, - }, - }, - author: { - connect: { id: self.id }, - }, - contentType: "SquiggleSnippet", - model: { - connect: { - id: model.id, - }, - }, - }, - }); - - return await tx.model.update({ - where: { - id: model.id, - }, - data: { - currentRevisionId: revision.id, - }, - }); - }); - - await indexModelId(model.id); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/deleteMembership.ts b/packages/hub/src/graphql/mutations/deleteMembership.ts deleted file mode 100644 index 6140c2aeb7..0000000000 --- a/packages/hub/src/graphql/mutations/deleteMembership.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { - getMembership, - getMyMembership, - groupHasAdminsBesidesUser, -} from "../helpers/groupHelpers"; - -builder.mutationField("deleteMembership", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("DeleteMembershipResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - errors: {}, - input: { - group: t.input.string({ required: true }), - user: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - // somewhat repetitive compared to `updateMembershipRole`, but with slightly different error messages - const myMembership = await getMyMembership({ - groupSlug: input.group, - session, - }); - - if (!myMembership) { - throw new Error("You're not a member of this group"); - } - - if ( - input.user !== session.user.username && - myMembership.role !== "Admin" - ) { - throw new Error("Only admins can delete other members"); - } - - const membershipToDelete = await getMembership({ - groupSlug: input.group, - userSlug: input.user, - }); - - if (!membershipToDelete) { - throw new Error(`${input.user} is not a member of ${input.group}`); - } - - if ( - !(await groupHasAdminsBesidesUser({ - groupSlug: input.group, - userSlug: input.user, - })) - ) { - throw new Error( - `Can't delete, ${input.user} is the last admin of ${input.group}` - ); - } - - await prisma.userGroupMembership.delete({ - where: { - id: membershipToDelete.id, - }, - }); - - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/deleteModel.tsx b/packages/hub/src/graphql/mutations/deleteModel.tsx deleted file mode 100644 index d903c53818..0000000000 --- a/packages/hub/src/graphql/mutations/deleteModel.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ZodError } from "zod"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; -import { getWriteableModel } from "../helpers/modelHelpers"; -import { validateSlug } from "../utils"; - -builder.mutationField("deleteModel", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("DeleteModelResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - input: { - owner: t.input.string({ required: true, validate: validateSlug }), - slug: t.input.string({ required: true, validate: validateSlug }), - }, - errors: { types: [ZodError, NotFoundError] }, - async resolve(_, { input }, { session }) { - const model = await getWriteableModel({ - slug: input.slug, - owner: input.owner, - session, - }); - - await prisma.model.delete({ - where: { id: model.id }, - }); - - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/deleteRelativeValuesDefinition.tsx b/packages/hub/src/graphql/mutations/deleteRelativeValuesDefinition.tsx deleted file mode 100644 index c084a90f44..0000000000 --- a/packages/hub/src/graphql/mutations/deleteRelativeValuesDefinition.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableOwnerBySlug } from "../helpers/ownerHelpers"; - -builder.mutationField("deleteRelativeValuesDefinition", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("DeleteRelativeValuesDefinitionResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - input: { - owner: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - }, - errors: {}, - async resolve(_, { input }, { session }) { - const owner = await getWriteableOwnerBySlug(session, input.owner); - - await prisma.relativeValuesDefinition.delete({ - where: { - slug_ownerId: { - slug: input.slug, - ownerId: owner.id, - }, - }, - }); - - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/deleteReusableGroupInviteToken.ts b/packages/hub/src/graphql/mutations/deleteReusableGroupInviteToken.ts deleted file mode 100644 index c1d0f36465..0000000000 --- a/packages/hub/src/graphql/mutations/deleteReusableGroupInviteToken.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { Group } from "../types/Group"; - -builder.mutationField("deleteReusableGroupInviteToken", (t) => - t.fieldWithInput({ - description: `Disable a reusable invite token for a group.`, - type: builder.simpleObject("DeleteReusableGroupInviteTokenResult", { - fields: (t) => ({ - group: t.field({ type: Group }), - }), - }), - errors: {}, - authScopes: (_, { input }) => ({ - isGroupAdminBySlug: input.slug, - }), - input: { - slug: t.input.string({ required: true }), - }, - resolve: async (_, { input }) => { - let group = await prisma.group.findFirstOrThrow({ - where: { - asOwner: { - slug: input.slug, - }, - }, - }); - - group = await prisma.group.update({ - where: { id: group.id }, - data: { reusableInviteToken: null }, - }); - - return { group }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/inviteUserToGroup.ts b/packages/hub/src/graphql/mutations/inviteUserToGroup.ts deleted file mode 100644 index 834e058e76..0000000000 --- a/packages/hub/src/graphql/mutations/inviteUserToGroup.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ZodError } from "zod"; - -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { MembershipRoleType } from "../types/Group"; -import { GroupInvite } from "../types/GroupInvite"; -import { validateSlug } from "../utils"; - -builder.mutationField("inviteUserToGroup", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("InviteUserToGroupResult", { - fields: (t) => ({ - invite: t.field({ type: GroupInvite }), - }), - }), - errors: { types: [ZodError] }, - input: { - group: t.input.string({ required: true, validate: validateSlug }), - username: t.input.string({ required: true, validate: validateSlug }), - role: t.input.field({ - type: MembershipRoleType, - required: true, - }), - }, - resolve: async (_, { input }, { session }) => { - const invite = await prisma.$transaction(async (tx) => { - const groupOwner = await tx.owner.findUnique({ - where: { - slug: input.group, - }, - }); - if (!groupOwner) { - throw new Error(`Group ${input.group} not found`); - } - - const invitedUser = await tx.user.findFirst({ - where: { - asOwner: { - slug: input.username, - }, - }, - }); - - if (!invitedUser) { - throw new Error(`Invited user ${input.username} not found`); - } - - // We perform all checks one by one because that allows more precise error reporting. - // (It would be possible to check everything in one big query with clever nested `connect` checks.) - const isAdmin = await tx.group.count({ - where: { - ownerId: groupOwner.id, - memberships: { - some: { - user: { email: session.user.email }, - role: "Admin", - }, - }, - }, - }); - if (!isAdmin) { - throw new Error(`You're not an admin of ${input.group} group`); - } - - const alreadyAMember = await tx.group.count({ - where: { - ownerId: groupOwner.id, - memberships: { - some: { userId: invitedUser.id }, - }, - }, - }); - if (alreadyAMember) { - throw new Error( - `${input.username} is already a member of ${input.group}` - ); - } - - const hasPendingInvite = await tx.group.count({ - where: { - ownerId: groupOwner.id, - invites: { - some: { - userId: invitedUser.id, - status: "Pending", - }, - }, - }, - }); - if (hasPendingInvite) { - throw new Error( - `There's already a pending invite for ${input.username} to join ${input.group}` - ); - } - - return await tx.groupInvite.create({ - data: { - user: { - connect: { id: invitedUser.id }, - }, - group: { - connect: { ownerId: groupOwner.id }, - }, - role: input.role, - }, - }); - }); - - return { invite }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/moveModel.ts b/packages/hub/src/graphql/mutations/moveModel.ts deleted file mode 100644 index 78e5d9ad9a..0000000000 --- a/packages/hub/src/graphql/mutations/moveModel.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ZodError } from "zod"; - -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { NotFoundError } from "../errors/NotFoundError"; -import { getWriteableModel } from "../helpers/modelHelpers"; -import { getWriteableOwnerBySlug } from "../helpers/ownerHelpers"; -import { Model } from "../types/Model"; -import { validateSlug } from "../utils"; - -builder.mutationField("moveModel", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("MoveModelResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: { types: [NotFoundError, ZodError] }, - input: { - oldOwner: t.input.string({ required: true, validate: validateSlug }), - newOwner: t.input.string({ required: true, validate: validateSlug }), - slug: t.input.string({ required: true, validate: validateSlug }), - }, - resolve: async (_, { input }, { session }) => { - let model = await getWriteableModel({ - owner: input.oldOwner, - slug: input.slug, - session, - }); - - const newOwner = await getWriteableOwnerBySlug(session, input.newOwner); - - model = await prisma.model.update({ - where: { id: model.id }, - data: { ownerId: newOwner.id }, - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/reactToGroupInvite.ts b/packages/hub/src/graphql/mutations/reactToGroupInvite.ts deleted file mode 100644 index dfc296be4a..0000000000 --- a/packages/hub/src/graphql/mutations/reactToGroupInvite.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { UserGroupMembership } from "../types/Group"; -import { GroupInvite } from "../types/GroupInvite"; -import { decodeGlobalIdWithTypename } from "../utils"; - -export const InviteReaction = builder.enumType("GroupInviteReaction", { - values: ["Accept", "Decline"], -}); - -builder.mutationField("reactToGroupInvite", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("ReactToGroupInviteResult", { - fields: (t) => ({ - invite: t.field({ type: GroupInvite }), - membership: t.field({ type: UserGroupMembership, nullable: true }), - }), - }), - errors: {}, - input: { - inviteId: t.input.string({ required: true }), - action: t.input.field({ type: InviteReaction, required: true }), - }, - resolve: async (_, { input }, { session }) => { - // Note: doesn't support email invites yet (which are not implemented anyway) - const decodedInviteId = decodeGlobalIdWithTypename( - input.inviteId, - "UserGroupInvite" - ); - - const newStatus = - input.action === "Accept" - ? "Accepted" - : input.action === "Decline" - ? "Declined" - : ("" as never); - - const { invite, membership } = await prisma.$transaction(async (tx) => { - const invite = await tx.groupInvite.update({ - where: { - id: decodedInviteId, - user: { - email: session.user.email, - }, - status: "Pending", - }, - data: { - status: newStatus, - }, - }); - - const membership = - invite.status === "Accepted" - ? await tx.userGroupMembership.create({ - data: { - group: { - connect: { - id: invite.groupId, - }, - }, - user: { - connect: { - email: session.user.email, - }, - }, - role: invite.role, - }, - }) - : null; - - return { invite, membership }; - }); - - return { invite, membership }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/setUsername.ts b/packages/hub/src/graphql/mutations/setUsername.ts deleted file mode 100644 index 4a1ab0ad0d..0000000000 --- a/packages/hub/src/graphql/mutations/setUsername.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ZodError } from "zod"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { Me } from "../types/Me"; -import { validateSlug } from "../utils"; - -builder.mutationField("setUsername", (t) => - t.withAuth({ signedIn: true }).field({ - type: Me, - args: { - username: t.arg.string({ required: true, validate: validateSlug }), - }, - errors: { types: [ZodError] }, - async resolve(_, args, { session }) { - if (session.user.username) { - throw new Error("Username is already set"); - } - - const existingOwner = await prisma.owner.count({ - where: { slug: args.username }, - }); - if (existingOwner) { - throw new Error(`Username ${args.username} is not available`); - } - - await prisma.user.update({ - where: { - email: session.user.email, - }, - data: { - asOwner: { - create: { slug: args.username }, - }, - }, - }); - - // I tried to call getSession() here to get a fresh session, but it didn't work; - // I suspect the reason is Next.js fetch() cache. - return { - ...session.user, - username: args.username, - }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateGroupInviteRole.ts b/packages/hub/src/graphql/mutations/updateGroupInviteRole.ts deleted file mode 100644 index 1ffdf8aac1..0000000000 --- a/packages/hub/src/graphql/mutations/updateGroupInviteRole.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MembershipRole } from "@prisma/client"; - -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { MembershipRoleType } from "../types/Group"; -import { GroupInvite } from "../types/GroupInvite"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("updateGroupInviteRole", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateGroupInviteRoleResult", { - fields: (t) => ({ - invite: t.field({ type: GroupInvite }), - }), - }), - errors: {}, - input: { - inviteId: t.input.string({ required: true }), - role: t.input.field({ - type: MembershipRoleType, - required: true, - }), - }, - resolve: async (_, { input }, { session }) => { - const user = await prisma.user.findUniqueOrThrow({ - where: { email: session.user.email }, - }); - - const decodedInviteId = decodeGlobalIdWithTypename(input.inviteId, [ - "UserGroupInvite", - "EmailGroupInvite", - ]); - - const invite = await prisma.groupInvite.findUniqueOrThrow({ - where: { id: decodedInviteId }, - include: { group: true }, - }); - if (invite.role === input.role) { - return { invite }; // nothing to do - } - - const myMembership = await prisma.userGroupMembership.findUnique({ - where: { - userId_groupId: { - userId: user.id, - groupId: invite.groupId, - }, - }, - }); - - if (myMembership?.role !== MembershipRole.Admin) { - throw new Error("You're not an admin of this group"); - } - - const updatedInvite = await prisma.groupInvite.update({ - where: { id: invite.id }, - data: { role: input.role }, - }); - - return { invite: updatedInvite }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateMembershipRole.ts b/packages/hub/src/graphql/mutations/updateMembershipRole.ts deleted file mode 100644 index a1d6f67eef..0000000000 --- a/packages/hub/src/graphql/mutations/updateMembershipRole.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { - getMembership, - getMyMembership, - groupHasAdminsBesidesUser, -} from "../helpers/groupHelpers"; -import { MembershipRoleType, UserGroupMembership } from "../types/Group"; - -builder.mutationField("updateMembershipRole", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateMembershipRoleResult", { - fields: (t) => ({ - membership: t.field({ type: UserGroupMembership }), - }), - }), - errors: {}, - input: { - group: t.input.string({ required: true }), - user: t.input.string({ required: true }), - role: t.input.field({ - type: MembershipRoleType, - required: true, - }), - }, - resolve: async (_, { input }, { session }) => { - // somewhat repetitive compared to `deleteMembership`, but with slightly different error messages - const myMembership = await getMyMembership({ - groupSlug: input.group, - session, - }); - - if (!myMembership) { - throw new Error("You're not a member of this group"); - } - - if ( - input.user !== session.user.username && - myMembership.role !== "Admin" - ) { - throw new Error("Only admins can update other members roles"); - } - - const membershipToUpdate = await getMembership({ - groupSlug: input.group, - userSlug: input.user, - }); - - if (!membershipToUpdate) { - throw new Error(`${input.user} is not a member of ${input.group}`); - } - - if (membershipToUpdate.role === input.role) { - return { membership: membershipToUpdate }; // nothing to do - } - - if ( - !(await groupHasAdminsBesidesUser({ - groupSlug: input.group, - userSlug: input.user, - })) - ) { - throw new Error( - `Can't change the role, ${input.user} is the last admin of ${input.group}` - ); - } - - const updatedMembership = await prisma.userGroupMembership.update({ - where: { id: membershipToUpdate.id }, - data: { role: input.role }, - }); - - return { membership: updatedMembership }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateModelPrivacy.ts b/packages/hub/src/graphql/mutations/updateModelPrivacy.ts deleted file mode 100644 index fddde8f499..0000000000 --- a/packages/hub/src/graphql/mutations/updateModelPrivacy.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableModel } from "../helpers/modelHelpers"; -import { Model } from "../types/Model"; - -builder.mutationField("updateModelPrivacy", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateModelPrivacyResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: {}, - input: { - owner: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - isPrivate: t.input.boolean({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - let model = await getWriteableModel({ - slug: input.slug, - owner: input.owner, - session, - }); - - model = await prisma.model.update({ - where: { id: model.id }, - data: { isPrivate: input.isPrivate }, - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateModelSlug.ts b/packages/hub/src/graphql/mutations/updateModelSlug.ts deleted file mode 100644 index 8c2494d876..0000000000 --- a/packages/hub/src/graphql/mutations/updateModelSlug.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableModel } from "../helpers/modelHelpers"; -import { Model } from "../types/Model"; - -builder.mutationField("updateModelSlug", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateModelSlugResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: {}, - input: { - owner: t.input.string({ required: true }), - oldSlug: t.input.string({ required: true }), - newSlug: t.input.string({ - required: true, - validate: { - regex: /^\w[\w\-]*$/, - }, - }), - }, - resolve: async (_, { input }, { session }) => { - let model = await getWriteableModel({ - owner: input.owner, - slug: input.oldSlug, - session, - }); - - model = await prisma.model.update({ - where: { id: model.id }, - data: { slug: input.newSlug }, - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateRelativeValuesDefinition.ts b/packages/hub/src/graphql/mutations/updateRelativeValuesDefinition.ts deleted file mode 100644 index 590db37f92..0000000000 --- a/packages/hub/src/graphql/mutations/updateRelativeValuesDefinition.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableOwnerBySlug } from "../helpers/ownerHelpers"; -import { RelativeValuesDefinition } from "../types/RelativeValuesDefinition"; -import { validateSlug } from "../utils"; -import { - RelativeValuesClusterInput, - RelativeValuesItemInput, - validateRelativeValuesDefinition, -} from "./createRelativeValuesDefinition"; - -builder.mutationField("updateRelativeValuesDefinition", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateRelativeValuesDefinitionResult", { - fields: (t) => ({ - definition: t.field({ - type: RelativeValuesDefinition, - nullable: false, - }), - }), - }), - errors: {}, - input: { - owner: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - title: t.input.string({ required: true }), - items: t.input.field({ - type: [RelativeValuesItemInput], - required: true, - }), - clusters: t.input.field({ - type: [RelativeValuesClusterInput], - required: true, - }), - recommendedUnit: t.input.string({ - validate: validateSlug, - }), - }, - resolve: async (_, { input }, { session }) => { - const owner = await getWriteableOwnerBySlug(session, input.owner); - - validateRelativeValuesDefinition({ - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - }); - - const definition = await prisma.$transaction(async (tx) => { - const revision = await tx.relativeValuesDefinitionRevision.create({ - data: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - definition: { - connect: { - slug_ownerId: { - slug: input.slug, - ownerId: owner.id, - }, - }, - }, - }, - include: { - definition: { - select: { - id: true, - }, - }, - }, - }); - - const definition = await tx.relativeValuesDefinition.update({ - where: { - id: revision.definition.id, - }, - data: { - currentRevisionId: revision.id, - }, - }); - - return definition; - }); - - return { definition }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts b/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts deleted file mode 100644 index a35d0ec4a8..0000000000 --- a/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { RelativeValuesDefinition } from "@prisma/client"; - -import { squiggleVersions } from "@quri/versioned-squiggle-components"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableModel } from "../helpers/modelHelpers"; -import { getSelf } from "../helpers/userHelpers"; -import { Model } from "../types/Model"; - -const DefinitionRefInput = builder.inputType("DefinitionRefInput", { - fields: (t) => ({ - owner: t.string({ required: true }), - slug: t.string({ required: true }), - }), -}); - -const RelativeValuesExportInput = builder.inputType( - "RelativeValuesExportInput", - { - fields: (t) => ({ - variableName: t.string({ required: true }), - definition: t.field({ - type: DefinitionRefInput, - required: true, - }), - }), - } -); - -const SquiggleSnippetContentInput = builder.inputType( - "SquiggleSnippetContentInput", - { - fields: (t) => ({ - code: t.string({ required: true }), - version: t.string({ required: true }), - seed: t.string({ required: true }), - autorunMode: t.boolean({ required: false }), - sampleCount: t.int({ required: false }), - xyPointLength: t.int({ required: false }), - }), - } -); - -builder.mutationField("updateSquiggleSnippetModel", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateSquiggleSnippetResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: {}, - input: { - owner: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - relativeValuesExports: t.input.field({ - type: [RelativeValuesExportInput], - }), - content: t.input.field({ - type: SquiggleSnippetContentInput, - required: true, - }), - comment: t.input.string(), - }, - resolve: async (_, { input }, { session }) => { - const existingModel = await getWriteableModel({ - slug: input.slug, - session, - owner: input.owner, - }); - - const version = input.content.version; - if (!(squiggleVersions as readonly string[]).includes(version)) { - throw new Error(`Unknown Squiggle version ${version}`); - } - - const relativeValuesExports = input.relativeValuesExports ?? []; - - const relativeValuesExportsToInsert: { - definitionId: string; - variableName: string; - }[] = []; - - if (relativeValuesExports.length) { - const selectedDefinitions = - await prisma.relativeValuesDefinition.findMany({ - where: { - OR: relativeValuesExports.map((pair) => ({ - slug: pair.definition.slug, - owner: { slug: pair.definition.owner }, - })), - }, - include: { owner: true }, - }); - - // username -> slug -> Definition - let linkedDefinitions: Map< - string, - Map - > = new Map(); - // now we need to match relativeValuesExports with definitions to get ids; I wonder if this could be simplified without sacrificing safety - for (const definition of selectedDefinitions) { - const { slug: ownerSlug } = definition.owner; - if (ownerSlug === null) { - continue; // should never happen - } - if (!linkedDefinitions.has(ownerSlug)) { - linkedDefinitions.set(ownerSlug, new Map()); - } - linkedDefinitions.get(ownerSlug)?.set(definition.slug, definition); - } - for (const pair of relativeValuesExports) { - const definition = linkedDefinitions - .get(pair.definition.owner) - ?.get(pair.definition.slug); - - if (!definition) { - throw new Error( - `Definition with owner ${pair.definition.owner}, slug ${pair.definition.slug} not found` - ); - } - relativeValuesExportsToInsert.push({ - variableName: pair.variableName, - definitionId: definition.id, - }); - } - } - - const self = await getSelf(session); - - const model = await prisma.$transaction(async (tx) => { - const revision = await tx.modelRevision.create({ - data: { - squiggleSnippet: { - create: { - code: input.content.code, - version: input.content.version, - seed: input.content.seed, - autorunMode: input.content.autorunMode ?? null, - sampleCount: input.content.sampleCount ?? null, - xyPointLength: input.content.xyPointLength ?? null, - }, - }, - contentType: "SquiggleSnippet", - comment: input.comment ?? "", - model: { - connect: { id: existingModel.id }, - }, - author: { - connect: { id: self.id }, - }, - relativeValuesExports: { - createMany: { - data: relativeValuesExportsToInsert, - }, - }, - }, - include: { - model: { - select: { - id: true, - }, - }, - }, - }); - const updatedModel = await tx.model.update({ - where: { - id: revision.modelId, - }, - data: { - currentRevisionId: revision.id, - }, - // TODO - optimize with queryFromInfo, https://pothos-graphql.dev/docs/plugins/prisma#optimized-queries-without-tprismafield - }); - - return updatedModel; - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/validateReusableGroupInviteToken.ts b/packages/hub/src/graphql/mutations/validateReusableGroupInviteToken.ts deleted file mode 100644 index 06e04e9d45..0000000000 --- a/packages/hub/src/graphql/mutations/validateReusableGroupInviteToken.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -// validate without accepting, for frontend checks -builder.mutationField("validateReusableGroupInviteToken", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("ValidateReusableGroupInviteTokenResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - errors: {}, - input: { - groupSlug: t.input.string({ required: true }), - inviteToken: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - let group = await prisma.group.findFirstOrThrow({ - where: { - asOwner: { - slug: input.groupSlug, - }, - }, - }); - - if (group.reusableInviteToken !== input.inviteToken) { - return { ok: false }; - } - - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/globalStatistics.ts b/packages/hub/src/graphql/queries/globalStatistics.ts deleted file mode 100644 index c9279304dd..0000000000 --- a/packages/hub/src/graphql/queries/globalStatistics.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -const GlobalStatistics = builder.simpleObject("GlobalStatistics", {}, (t) => ({ - users: t.int({ - async resolve() { - return await prisma.user.count(); - }, - }), - models: t.int({ - async resolve() { - return await prisma.model.count(); - }, - }), - relativeValuesDefinitions: t.int({ - async resolve() { - return await prisma.relativeValuesDefinition.count(); - }, - }), -})); - -builder.queryField("globalStatistics", (t) => - t.field({ - type: GlobalStatistics, - resolve: () => ({}), - }) -); diff --git a/packages/hub/src/graphql/queries/group.ts b/packages/hub/src/graphql/queries/group.ts deleted file mode 100644 index f3bdbe5599..0000000000 --- a/packages/hub/src/graphql/queries/group.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; - -builder.queryField("group", (t) => - t.prismaField({ - type: "Group", - args: { - slug: t.arg.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, args) { - const group = await prisma.group.findFirst({ - ...query, - where: { - asOwner: { - slug: args.slug, - }, - }, - }); - if (!group) { - throw new NotFoundError(); - } - return group; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/groups.ts b/packages/hub/src/graphql/queries/groups.ts deleted file mode 100644 index e264925eb7..0000000000 --- a/packages/hub/src/graphql/queries/groups.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { isSignedIn } from "../helpers/userHelpers"; -import { Group, GroupConnection } from "../types/Group"; - -const GroupsQueryInput = builder.inputType("GroupsQueryInput", { - fields: (t) => ({ - slugContains: t.string(), - myOnly: t.boolean({ - description: "List only groups that you're a member of", - }), - }), -}); - -builder.queryField("groups", (t) => - t.prismaConnection( - { - type: Group, - cursor: "id", - args: { - input: t.arg({ type: GroupsQueryInput }), - }, - resolve: async (query, _, { input }, { session }) => { - if (input?.myOnly && !isSignedIn(session)) { - // Relay stuggles with union types on connection fields (see e.g. https://github.com/facebook/relay/issues/4366) - // So we return an empty list instead of throwing an error. - return []; - } - - return await prisma.group.findMany({ - ...query, - orderBy: { - updatedAt: "desc", - }, - where: { - ...(input?.slugContains && { - asOwner: { - slug: { - contains: input.slugContains, - mode: "insensitive", - }, - }, - }), - ...(input?.myOnly && - isSignedIn(session) && { - memberships: { - some: { - user: { email: session.user.email }, - }, - }, - }), - }, - }); - }, - }, - GroupConnection - ) -); diff --git a/packages/hub/src/graphql/queries/me.ts b/packages/hub/src/graphql/queries/me.ts deleted file mode 100644 index 9d906d9446..0000000000 --- a/packages/hub/src/graphql/queries/me.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { builder } from "@/graphql/builder"; - -import { Me } from "../types/Me"; - -builder.queryField("me", (t) => - t.withAuth({ signedIn: true }).field({ - type: Me, - async resolve(_, __, { session }) { - return session.user; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/model.ts b/packages/hub/src/graphql/queries/model.ts deleted file mode 100644 index 534c32d39f..0000000000 --- a/packages/hub/src/graphql/queries/model.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; -import { modelWhereHasAccess } from "../helpers/modelHelpers"; - -builder.queryField("model", (t) => - t.prismaFieldWithInput({ - type: "Model", - input: { - slug: t.input.string({ required: true }), - owner: t.input.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, { input }, { session }) { - const model = await prisma.model.findFirst({ - ...query, - where: { - slug: input.slug, - owner: { slug: input.owner }, - // intentionally checking access - see https://github.com/quantified-uncertainty/squiggle/issues/3414 - ...modelWhereHasAccess(session), - }, - }); - if (!model) { - throw new NotFoundError(); - } - return model; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/models.ts b/packages/hub/src/graphql/queries/models.ts deleted file mode 100644 index 8c5d778046..0000000000 --- a/packages/hub/src/graphql/queries/models.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { Model, ModelConnection } from "../types/Model"; - -builder.queryField("models", (t) => - t.prismaConnection( - { - type: Model, - cursor: "id", - resolve: (query, _, __, { session }) => { - return prisma.model.findMany({ - ...query, - orderBy: { - updatedAt: "desc", - }, - where: modelWhereHasAccess(session), - }); - }, - }, - ModelConnection - ) -); diff --git a/packages/hub/src/graphql/queries/modelsByVersion.ts b/packages/hub/src/graphql/queries/modelsByVersion.ts deleted file mode 100644 index a97641aa60..0000000000 --- a/packages/hub/src/graphql/queries/modelsByVersion.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { Model } from "../types/Model"; - -const ModelsByVersion = builder.simpleObject("ModelsByVersion", { - fields: (t) => ({ - version: t.string(), - count: t.int(), - privateCount: t.int(), - models: t.field({ - type: [Model], - }), - }), -}); - -builder.queryField("modelsByVersion", (t) => - t.field({ - description: "Admin-only query for listing models in /admin UI", - type: [ModelsByVersion], - authScopes: { - isRootUser: true, - }, - resolve: async () => { - const models = await prisma.model.findMany({ - include: { - currentRevision: { - where: { - contentType: "SquiggleSnippet", - }, - include: { - squiggleSnippet: true, - }, - }, - }, - }); - - const groupedModels: Record = {}; - const privateStats: Record = {}; - const versions = new Set(); - - for (const model of models) { - const version = model.currentRevision?.squiggleSnippet?.version; - if (!version) continue; - - versions.add(version); - - if (model.isPrivate) { - privateStats[version] ??= 0; - privateStats[version]++; - } else { - // don't expose private models, they won't be available through GraphQL anyway - groupedModels[version] ??= []; - groupedModels[version].push(model); - } - } - return [...versions.values()].map((version) => ({ - version, - count: (groupedModels[version] ?? []).length, - privateCount: privateStats[version] ?? 0, - models: groupedModels[version] ?? [], - })); - }, - }) -); diff --git a/packages/hub/src/graphql/queries/relativeValuesDefinition.ts b/packages/hub/src/graphql/queries/relativeValuesDefinition.ts deleted file mode 100644 index edc14662e4..0000000000 --- a/packages/hub/src/graphql/queries/relativeValuesDefinition.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; - -builder.queryField("relativeValuesDefinition", (t) => - t.prismaFieldWithInput({ - type: "RelativeValuesDefinition", - input: { - slug: t.input.string({ required: true }), - owner: t.input.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, args) { - const definition = await prisma.relativeValuesDefinition.findFirst({ - ...query, - where: { - slug: args.input.slug, - owner: { slug: args.input.owner }, - }, - }); - if (!definition) { - throw new NotFoundError(); - } - return definition; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/relativeValuesDefinitions.ts b/packages/hub/src/graphql/queries/relativeValuesDefinitions.ts deleted file mode 100644 index 2e80d2827a..0000000000 --- a/packages/hub/src/graphql/queries/relativeValuesDefinitions.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { - RelativeValuesDefinition, - RelativeValuesDefinitionConnection, -} from "../types/RelativeValuesDefinition"; - -const RelativeValuesDefinitionsQueryInput = builder.inputType( - "RelativeValuesDefinitionsQueryInput", - { - fields: (t) => ({ - slugContains: t.string(), - owner: t.string(), - }), - } -); - -builder.queryField("relativeValuesDefinitions", (t) => - t.prismaConnection( - { - type: RelativeValuesDefinition, - cursor: "id", - args: { - input: t.arg({ type: RelativeValuesDefinitionsQueryInput }), - }, - resolve: (query, _, args) => - prisma.relativeValuesDefinition.findMany({ - ...query, - where: { - ...(args.input?.slugContains && { - slug: { - contains: args.input.slugContains, - mode: "insensitive", - }, - }), - ...(args.input?.owner && { - owner: { slug: args.input.owner }, - }), - }, - orderBy: { updatedAt: "desc" }, - }), - }, - RelativeValuesDefinitionConnection - ) -); diff --git a/packages/hub/src/graphql/queries/search.ts b/packages/hub/src/graphql/queries/search.ts deleted file mode 100644 index dd9b1c7237..0000000000 --- a/packages/hub/src/graphql/queries/search.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; -import { Searchable } from "../types/Searchable"; -import { Searchable as PrismaSearchable } from "@prisma/client"; -import { resolveOffsetConnection } from "@pothos/plugin-relay"; - -type SearchableWithEdgeData = PrismaSearchable & { - rank: number; - slugSnippet: string; - textSnippet: string; -}; - -const SearchEdge = builder.edgeObject({ - type: Searchable, - name: "SearchEdge", - fields: (t) => ({ - rank: t.float({ - resolve: (obj) => { - const rank = (obj.node as SearchableWithEdgeData).rank; - return rank ?? 1e9; - }, - }), - slugSnippet: t.string({ - resolve: (obj) => { - const snippet = (obj.node as SearchableWithEdgeData).slugSnippet; - return String(snippet ?? ""); - }, - }), - textSnippet: t.string({ - resolve: (obj) => { - const snippet = (obj.node as SearchableWithEdgeData).textSnippet; - return String(snippet ?? ""); - }, - }), - }), -}); - -builder.queryField("search", (t) => - t.connection( - { - type: Searchable, - args: { - text: t.arg.string({ required: true }), - }, - errors: {}, - resolve: async (_, args, { session }) => { - return await resolveOffsetConnection( - { args }, - async ({ limit, offset }) => { - const { text } = args; - return await prisma.$queryRaw` - SELECT - "Searchable".*, - -- ranking function - ts_rank_cd( - setweight(to_tsvector(coalesce("Model".slug, '')), 'A') || - setweight(to_tsvector(coalesce("SquiggleSnippet".code, '')), 'B') || - setweight(to_tsvector(coalesce("RelativeValuesDefinition".slug, '')), 'A') || - setweight(to_tsvector(coalesce("UserOwner".slug, '')), 'A') || - setweight(to_tsvector(coalesce("GroupOwner".slug, '')), 'A'), - websearch_to_tsquery(${text}) - ) AS rank, - ts_headline( - concat_ws( - ' ', - "Model".slug, - "RelativeValuesDefinition".slug, - "UserOwner".slug, - "GroupOwner".slug - ), - websearch_to_tsquery(${text}), - 'HighlightAll=t' - ) AS "slugSnippet", - ts_headline(concat_ws( - ' ', - "SquiggleSnippet".code - ), websearch_to_tsquery(${text}) - ) AS "textSnippet" - FROM "Searchable" - LEFT JOIN "Model" ON "Model".id = "Searchable"."modelId" - LEFT JOIN "Owner" AS "ModelOwner" ON "Model"."ownerId" = "ModelOwner".id - LEFT JOIN "User" AS "ModelOwnerUser" ON "ModelOwner".id = "ModelOwnerUser"."ownerId" - LEFT JOIN "Group" AS "ModelOwnerGroup" ON "ModelOwner".id = "ModelOwnerGroup"."ownerId" - LEFT JOIN "ModelRevision" ON "Model"."currentRevisionId" = "ModelRevision".id - LEFT JOIN "SquiggleSnippet" ON "ModelRevision"."contentId" = "SquiggleSnippet".id - LEFT JOIN "RelativeValuesDefinition" ON "RelativeValuesDefinition".id = "Searchable"."definitionId" - -- LEFT JOIN "Owner" AS "RelativeValuesDefinitionOwner" ON "RelativeValuesDefinition"."ownerId" = "RelativeValuesDefinitionOwner".id - LEFT JOIN "User" ON "User".id = "Searchable"."userId" - LEFT JOIN "Owner" AS "UserOwner" ON "User"."ownerId" = "UserOwner".id - LEFT JOIN "Group" ON "Group".id = "Searchable"."groupId" - LEFT JOIN "Owner" AS "GroupOwner" ON "Group"."ownerId" = "GroupOwner".id - WHERE - ( - -- check permissions, should match "modelWhereHasAccess" function - "Model".id IS NULL OR - "Model"."isPrivate" IS FALSE - OR "ModelOwnerUser".email = ${session?.user.email} - OR "ModelOwnerGroup".id IN ( - SELECT DISTINCT "groupId" - FROM "UserGroupMembership" - LEFT JOIN "User" ON "User".id = "UserGroupMembership"."userId" - WHERE "User".email = ${session?.user.email} - ) - ) AND ( - -- construct search document - to_tsvector(concat_ws( - ' ', - "Model".slug, - "SquiggleSnippet".code, - "RelativeValuesDefinition".slug, - "UserOwner".slug, - "GroupOwner".slug - )) @@ websearch_to_tsquery(${text}) - ) - ORDER BY rank DESC - LIMIT ${limit} - OFFSET ${offset} - `; - } - ); - }, - }, - {}, - SearchEdge - ) -); diff --git a/packages/hub/src/graphql/queries/userByUsername.ts b/packages/hub/src/graphql/queries/userByUsername.ts deleted file mode 100644 index a542567b29..0000000000 --- a/packages/hub/src/graphql/queries/userByUsername.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; -import { User } from "../types/User"; - -builder.queryField("userByUsername", (t) => - t.prismaField({ - type: User, - args: { - username: t.arg.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, args) { - const user = await prisma.user.findFirst({ - ...query, - where: { - asOwner: { - slug: args.username, - }, - }, - }); - if (!user) { - throw new NotFoundError(`User ${args.username} not found`); - } - return user; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/users.ts b/packages/hub/src/graphql/queries/users.ts deleted file mode 100644 index 35620d1620..0000000000 --- a/packages/hub/src/graphql/queries/users.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Prisma } from "@prisma/client"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { User } from "../types/User"; - -const UsersQueryInput = builder.inputType("UsersQueryInput", { - fields: (t) => ({ - usernameContains: t.string(), - }), -}); - -builder.queryField("users", (t) => - t.prismaConnection({ - type: User, - cursor: "id", - args: { - input: t.arg({ type: UsersQueryInput }), - }, - resolve: async (query, _, { input }) => { - const where: Prisma.UserWhereInput = { - ownerId: { not: null }, - }; - - if (input?.usernameContains) { - where.asOwner = { - slug: { - contains: input.usernameContains, - mode: "insensitive", - }, - }; - } - - return await prisma.user.findMany({ - ...query, - where, - }); - }, - }) -); diff --git a/packages/hub/src/graphql/queries/variable.ts b/packages/hub/src/graphql/queries/variable.ts deleted file mode 100644 index f1d8e888b8..0000000000 --- a/packages/hub/src/graphql/queries/variable.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; - -builder.queryField("variable", (t) => - t.prismaFieldWithInput({ - type: "Variable", - input: { - variableName: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - owner: t.input.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, { input }) { - const model = await prisma.model.findFirst({ - where: { - slug: input.slug, - owner: { slug: input.owner }, - }, - }); - - if (!model) { - throw new NotFoundError(); - } - - const variable = await prisma.variable.findFirst({ - ...query, - where: { - variableName: input.variableName, - modelId: model.id, - }, - }); - - if (!variable) { - throw new NotFoundError(); - } - - return variable; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/variables.ts b/packages/hub/src/graphql/queries/variables.ts deleted file mode 100644 index 7470053ef9..0000000000 --- a/packages/hub/src/graphql/queries/variables.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { Variable, VariableConnection } from "../types/Variable"; - -const VariableQueryInput = builder.inputType("VariableQueryInput", { - fields: (t) => ({ - modelId: t.string(), - variableName: t.string(), - owner: t.string(), - variableType: t.string(), - }), -}); - -builder.queryField("variables", (t) => - t.prismaConnection( - { - type: Variable, - cursor: "id", - args: { - input: t.arg({ type: VariableQueryInput }), - }, - resolve: (query, _, { input }, { session }) => { - const modelId = input?.modelId; - - const queries = { - model: { - ...modelWhereHasAccess(session), - ...(input?.owner && { owner: { slug: input.owner } }), - }, - ...(modelId && { modelId: modelId }), - ...(input?.variableName && { variableName: input.variableName }), - ...(input?.variableType && { - currentRevision: { - variableType: input.variableType, - }, - }), - }; - - return prisma.variable.findMany({ - ...query, - where: { - ...queries, - }, - }); - }, - }, - VariableConnection - ) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts deleted file mode 100644 index 6a9c395ef4..0000000000 --- a/packages/hub/src/graphql/schema.ts +++ /dev/null @@ -1,48 +0,0 @@ -import "./errors/BaseError"; -import "./errors/NotFoundError"; -import "./errors/ValidationError"; -import "./queries/globalStatistics"; -import "./queries/group"; -import "./queries/groups"; -import "./queries/me"; -import "./queries/model"; -import "./queries/variables"; -import "./queries/variable"; -import "./queries/models"; -import "./queries/modelsByVersion"; -import "./queries/relativeValuesDefinition"; -import "./queries/relativeValuesDefinitions"; -import "./queries/runSquiggle"; -import "./queries/search"; -import "./queries/userByUsername"; -import "./queries/users"; -import "./mutations/acceptReusableGroupInviteToken"; -import "./mutations/adminUpdateModelVersion"; -import "./mutations/adminRebuildSearchIndex"; -import "./mutations/buildRelativeValuesCache"; -import "./mutations/cancelGroupInvite"; -import "./mutations/clearRelativeValuesCache"; -import "./mutations/createGroup"; -import "./mutations/createRelativeValuesDefinition"; -import "./mutations/createReusableGroupInviteToken"; -import "./mutations/createSquiggleSnippetModel"; -import "./mutations/deleteMembership"; -import "./mutations/deleteModel"; -import "./mutations/deleteRelativeValuesDefinition"; -import "./mutations/deleteReusableGroupInviteToken"; -import "./mutations/addUserToGroup"; -import "./mutations/inviteUserToGroup"; -import "./mutations/moveModel"; -import "./mutations/reactToGroupInvite"; -import "./mutations/setUsername"; -import "./mutations/updateGroupInviteRole"; -import "./mutations/updateMembershipRole"; -import "./mutations/updateModelPrivacy"; -import "./mutations/updateModelSlug"; -import "./mutations/updateRelativeValuesDefinition"; -import "./mutations/updateSquiggleSnippetModel"; -import "./mutations/validateReusableGroupInviteToken"; - -import { builder } from "./builder"; - -export const schema = builder.toSchema(); diff --git a/packages/hub/src/graphql/types/Group.ts b/packages/hub/src/graphql/types/Group.ts deleted file mode 100644 index 774f51bf70..0000000000 --- a/packages/hub/src/graphql/types/Group.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; -import { MembershipRole } from "@prisma/client"; - -import { builder } from "../builder"; -import { getMyMembershipById } from "../helpers/groupHelpers"; -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { GroupInvite, GroupInviteConnection } from "./GroupInvite"; -import { ModelConnection, modelConnectionHelpers } from "./Model"; -import { Owner } from "./Owner"; -import { variableRevisionConnectionHelpers } from "./VariableRevision"; - -export const MembershipRoleType = builder.enumType(MembershipRole, { - name: "MembershipRole", -}); - -export const UserGroupMembership = builder.prismaNode("UserGroupMembership", { - id: { field: "id" }, - fields: (t) => ({ - role: t.field({ - type: MembershipRoleType, - resolve: (t) => t.role, - }), - user: t.relation("user"), - group: t.relation("group"), - }), -}); - -export const UserGroupMembershipConnection = builder.connectionObject({ - type: UserGroupMembership, - name: "UserGroupMembershipConnection", -}); - -export const Group = builder.prismaNode("Group", { - id: { field: "id" }, - interfaces: [Owner], - fields: (t) => ({ - slug: t.string({ - select: { asOwner: true }, - resolve: (group) => group.asOwner.slug, - }), - createdAtTimestamp: t.float({ - resolve: (group) => group.createdAt.getTime(), - }), - updatedAtTimestamp: t.float({ - resolve: (group) => group.updatedAt.getTime(), - }), - reusableInviteToken: t.exposeString("reusableInviteToken", { - nullable: true, - unauthorizedResolver: () => null, - authScopes: (group) => ({ - isGroupAdmin: group.id, - }), - }), - myMembership: t.field({ - type: UserGroupMembership, - nullable: true, - resolve: async (root, _, { session }) => { - // TODO - `prismaField`? - // TODO - this causes an extra query, optimize somehow? - return getMyMembershipById(root.id, session); - }, - }), - // Note: I also tried `members` field with membership data exposed on the connection edges, but had issues with it: - // 1. Edge ids were not Relay ids - // 2. I couldn't make Pothos accept an interface for edge type, which caused duplication in GraphQL types. - // Maybe Pothos will improve the support for it in the future: https://pothos-graphql.dev/docs/plugins/prisma#indirect-relations-as-connections - memberships: t.relatedConnection( - "memberships", - { cursor: "id" }, - UserGroupMembershipConnection - ), - invites: t.relatedConnection( - "invites", - { - cursor: "id", - nullable: true, // "null" means "forbidden" - authScopes: (group) => ({ - // It would be nice to select membership at top level of Group object, - // since this is also useful for `myMembership` field. - // But unfortunately Prisma doesn't have select aliases: - // https://github.com/prisma/prisma/discussions/14316 - // https://github.com/prisma/prisma/issues/8151 - isGroupAdmin: group.id, - }), - unauthorizedResolver: () => null, - query: () => ({ - orderBy: { - createdAt: "desc", - }, - where: { - status: "Pending", - }, - }), - }, - GroupInviteConnection - ), - inviteForMe: t.withAuth({ signedIn: true }).field({ - type: GroupInvite, - nullable: true, - unauthorizedResolver: () => null, - select: (_, { session }) => ({ - invites: { - where: { - user: { - email: session?.user.email, - }, - status: "Pending", - }, - }, - }), - resolve: (group) => group.invites[0], - }), - // Models are stored on owner.models, wo we have to use indirect relation (https://pothos-graphql.dev/docs/plugins/prisma#indirect-relations-as-connections) - // See also: User.models field. - models: t.connection( - { - type: modelConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - models: { - ...modelConnectionHelpers.getQuery(args, ctx, nestedSelection), - where: modelWhereHasAccess(ctx.session), - orderBy: { updatedAt: "desc" }, - }, - }, - }, - }), - resolve: (user, args, ctx) => - modelConnectionHelpers.resolve(user.asOwner.models, args, ctx), - }, - ModelConnection - ), - variableRevisions: t.connection( - { - type: variableRevisionConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - models: { - select: { - currentRevision: { - select: { - variableRevisions: { - ...variableRevisionConnectionHelpers.getQuery( - args, - ctx, - nestedSelection - ), - }, - }, - }, - }, - }, - }, - }, - }), - resolve: (group, args, ctx) => { - const variableRevisions = - group.asOwner?.models - .map((model) => model.currentRevision?.variableRevisions ?? []) - .flat() ?? []; - return variableRevisionConnectionHelpers.resolve( - variableRevisions, - args, - ctx - ); - }, - }, - variableRevisionConnectionHelpers.ref - ), - }), -}); - -export const GroupConnection = builder.connectionObject({ - type: Group, - name: "GroupConnection", -}); - -// useful when we want to expose `groups` field on a `User`, instead of `memberships` -export const groupFromMembershipConnectionHelpers = prismaConnectionHelpers( - builder, - "UserGroupMembership", - { - cursor: "id", - select: (nodeSelection) => ({ - group: nodeSelection(), - }), - resolveNode: (node) => node.group, - } -); diff --git a/packages/hub/src/graphql/types/GroupInvite.ts b/packages/hub/src/graphql/types/GroupInvite.ts deleted file mode 100644 index f402b2793f..0000000000 --- a/packages/hub/src/graphql/types/GroupInvite.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { builder } from "../builder"; -import { MembershipRoleType } from "./Group"; - -export const GroupInvite = builder.prismaInterface("GroupInvite", { - fields: (t) => ({ - id: t.exposeID("id"), - group: t.relation("group"), - role: t.field({ - type: MembershipRoleType, - resolve: (t) => t.role, - }), - }), - resolveType: (invite) => { - if (invite.userId) { - return "UserGroupInvite"; - } else if (invite.email) { - return "EmailGroupInvite"; - } else { - throw new Error("Invalid invite object"); - } - }, -}); - -builder.prismaNode("GroupInvite", { - variant: "UserGroupInvite", - interfaces: [GroupInvite], - id: { field: "id" }, - fields: (t) => ({ - user: t.relation("user"), - }), -}); - -builder.prismaNode("GroupInvite", { - variant: "EmailGroupInvite", - interfaces: [GroupInvite], - id: { field: "id" }, - fields: (t) => ({ - email: t.exposeString("email", { - // Pothos workaround because we guarantee that email invites have non-null email values - nullable: false as any, - }), - }), -}); - -export const GroupInviteConnection = builder.connectionObject({ - type: GroupInvite, - name: "GroupInviteConnection", -}); diff --git a/packages/hub/src/graphql/types/Me.ts b/packages/hub/src/graphql/types/Me.ts deleted file mode 100644 index 7be70238cd..0000000000 --- a/packages/hub/src/graphql/types/Me.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { User } from "./User"; - -export const Me = builder - .objectRef<{ email: string; username?: string | undefined | null }>("Me") - .implement({ - authScopes: { - signedIn: true, - }, - fields: (t) => ({ - email: t.exposeString("email"), - username: t.exposeString("username", { nullable: true }), - asUser: t.withAuth({ signedIn: true }).prismaField({ - type: User, - resolve: async (query, _, __, { session }) => { - return await prisma.user.findUniqueOrThrow({ - ...query, - where: { email: session.user.email }, - }); - }, - }), - }), - }); diff --git a/packages/hub/src/graphql/types/Model.ts b/packages/hub/src/graphql/types/Model.ts deleted file mode 100644 index d08db10789..0000000000 --- a/packages/hub/src/graphql/types/Model.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; - -import { builder } from "@/graphql/builder"; - -import { decodeGlobalIdWithTypename } from "../utils"; -import { ModelRevision, ModelRevisionConnection } from "./ModelRevision"; -import { Owner } from "./Owner"; - -export const Model = builder.prismaNode("Model", { - id: { field: "id" }, - - authScopes: (model) => { - if (!model.isPrivate) { - return true; - } - - // This might leak the info that the model exists, but we handle that in `model()` query and return NotFoundError. - // It's probable that we leak this info somewhere else, though. - return { controlsOwnerId: model.ownerId }; - }, - - fields: (t) => ({ - slug: t.exposeString("slug"), - // I'm not yet sure if we'll use custom scalars for datetime encoding, so `createdAtTimestamp` is a precaution; we'll probably switch to `createAt` in the future - createdAtTimestamp: t.float({ - resolve: (model) => model.createdAt.getTime(), - }), - updatedAtTimestamp: t.float({ - resolve: (model) => model.updatedAt.getTime(), - }), - owner: t.field({ - type: Owner, - // TODO - we need to extract fragment data from Owner query and call nestedSelection(...) for optimal performance. - select: { - owner: { - include: { - user: true, - group: true, - }, - }, - }, - resolve: (model) => { - const result = model.owner.user ?? model.owner.group; - if (!result) { - throw new Error("Invalid owner object, missing user or group"); - } - // necessary for Owner type - (result as any)["_owner"] = { - type: model.owner.user ? "User" : "Group", - }; - return result; - }, - }), - isPrivate: t.exposeBoolean("isPrivate"), - isEditable: t.boolean({ - authScopes: (model) => ({ - controlsOwnerId: model.ownerId, - }), - resolve: () => true, - unauthorizedResolver: () => false, - }), - currentRevision: t.relation("currentRevision", { - nullable: false, - }), - - revision: t.field({ - type: ModelRevision, - args: { - id: t.arg.id({ required: true }), - }, - select: (args, _, nestedSelection) => { - const id = decodeGlobalIdWithTypename(String(args.id), "ModelRevision"); - - return { - revisions: nestedSelection({ - // necessary for ModelRevision authScopes - include: { model: true }, - take: 1, - where: { id }, - }), - }; - }, - async resolve(model) { - const revision = model.revisions[0]; - if (!revision) { - throw new Error("Not found"); - } - return revision; - }, - }), - revisions: t.relatedConnection( - "revisions", - { - cursor: "id", - query: () => ({ - orderBy: { - createdAt: "desc", - }, - }), - }, - ModelRevisionConnection - ), - variables: t.relation("variables"), - lastRevisionWithBuild: t.field({ - type: ModelRevision, - nullable: true, - select: (args, ctx, nestedSelection) => ({ - revisions: nestedSelection({ - include: { - // required by ModelRevision authScopes - model: true, - }, - orderBy: { - createdAt: "desc", - }, - where: { - builds: { - some: { - id: { - not: undefined, - }, - }, - }, - }, - take: 1, - }), - }), - async resolve(model) { - return model.revisions[0]; - }, - }), - }), -}); - -export const ModelConnection = builder.connectionObject({ - type: Model, - name: "ModelConnection", -}); - -export const modelConnectionHelpers = prismaConnectionHelpers( - builder, - "Model", - { cursor: "id" } -); diff --git a/packages/hub/src/graphql/types/ModelRevision.ts b/packages/hub/src/graphql/types/ModelRevision.ts deleted file mode 100644 index 4c21907f5f..0000000000 --- a/packages/hub/src/graphql/types/ModelRevision.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { UnionRef } from "@pothos/core"; - -import { ASTNode, parse } from "@quri/squiggle-lang"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; -import { ModelRevisionBuild } from "./ModelRevisionBuild"; -import { RelativeValuesExport } from "./RelativeValuesExport"; -import { SquiggleSnippet } from "./SquiggleSnippet"; - -// TODO - turn into interface? -const ModelContent: UnionRef< - { - id: string; - code: string; - version: string; - }, - { - id: string; - code: string; - version: string; - } -> = builder.unionType("ModelContent", { - types: [SquiggleSnippet], - resolveType: () => SquiggleSnippet, -}); - -const ModelRevisionBuildStatus = builder.enumType("ModelRevisionBuildStatus", { - values: ["Skipped", "Pending", "Success", "Failure"], -}); - -function astToVariableNames(ast: ASTNode): string[] { - const exportedVariableNames: string[] = []; - - if (ast.kind === "Program") { - ast.statements.forEach((statement) => { - if ( - (statement.kind === "LetStatement" || - statement.kind === "DefunStatement") && - statement.exported - ) { - exportedVariableNames.push(statement.variable.value); - } - }); - } - - return exportedVariableNames; -} - -export function getExportedVariableNames(code: string): string[] { - const ast = parse(code); - if (ast.ok) { - return astToVariableNames(ast.value); - } else { - return []; - } -} - -export const ModelRevision = builder.prismaNode("ModelRevision", { - id: { field: "id" }, - - include: { model: true }, - authScopes: (revision) => { - if (!revision.model.isPrivate) { - return true; - } - return { controlsOwnerId: revision.model.ownerId }; - }, - - fields: (t) => ({ - createdAtTimestamp: t.float({ - resolve: (revision) => revision.createdAt.getTime(), - }), - // `relatedConnection` would be more principled, and in theory the number of variables with definitions could be high. - // But connection is harder to deal with on the UI side, and since we send all variables back on updates, so it doesn't make much sense there. - relativeValuesExports: t.relation("relativeValuesExports"), - variableRevisions: t.relation("variableRevisions"), - model: t.relation("model"), - - lastBuild: t.field({ - type: ModelRevisionBuild, - nullable: true, - select: { - builds: { - orderBy: { - createdAt: "desc", - }, - take: 1, - }, - }, - async resolve(revision) { - return revision.builds[0]; - }, - }), - author: t.relation("author", { nullable: true }), - comment: t.exposeString("comment"), - buildStatus: t.field({ - type: ModelRevisionBuildStatus, - select: { - builds: { - select: { - errors: true, - }, - orderBy: { - createdAt: "desc", - }, - take: 1, - }, - model: { - select: { - currentRevisionId: true, - }, - }, - }, - async resolve(revision) { - const lastBuild = revision.builds[0]; - - if (lastBuild) { - const errors = lastBuild.errors.filter((e) => e !== ""); - return errors.length === 0 ? "Success" : "Failure"; - } - - return revision.model.currentRevisionId === revision.id - ? "Pending" - : "Skipped"; - }, - }), - content: t.field({ - type: ModelContent, - select: { squiggleSnippet: true }, - async resolve(revision) { - switch (revision.contentType) { - case "SquiggleSnippet": - return revision.squiggleSnippet!; - } - }, - }), - exportNames: t.field({ - type: ["String"], - select: { squiggleSnippet: true }, - async resolve(revision) { - if (revision.contentType === "SquiggleSnippet") { - return getExportedVariableNames(revision.squiggleSnippet!.code); - } else { - return []; - } - }, - }), - forRelativeValues: t.fieldWithInput({ - type: RelativeValuesExport, - errors: { - types: [NotFoundError], - }, - input: { - variableName: t.input.string({ required: true }), - // optional, necessary if the variable is associated with multiple definitions - for: t.input.field({ - type: builder.inputType( - "ModelRevisionForRelativeValuesSlugOwnerInput", - { - fields: (t) => ({ - slug: t.string({ required: true }), - owner: t.string({ required: true }), - }), - } - ), - }), - }, - async resolve(revision, { input }) { - const exports = await prisma.relativeValuesExport.findMany({ - where: { - modelRevisionId: revision.id, - variableName: input.variableName, - ...(input.for - ? { - definition: { - owner: { slug: input.for.owner }, - slug: input.for.slug, - }, - } - : {}), - }, - include: { - definition: true, - }, - }); - - if (exports.length > 1) { - throw new Error("Ambiguous input, multiple variables match it"); - } - - if (exports.length === 0) { - throw new NotFoundError(); - } - - return exports[0]; - }, - }), - }), -}); - -export const ModelRevisionConnection = builder.connectionObject({ - type: ModelRevision, - name: "ModelRevisionConnection", -}); diff --git a/packages/hub/src/graphql/types/ModelRevisionBuild.ts b/packages/hub/src/graphql/types/ModelRevisionBuild.ts deleted file mode 100644 index 56c5d7bb39..0000000000 --- a/packages/hub/src/graphql/types/ModelRevisionBuild.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { builder } from "@/graphql/builder"; - -export const ModelRevisionBuild = builder.prismaNode("ModelRevisionBuild", { - id: { field: "id" }, - fields: (t) => ({ - createdAtTimestamp: t.float({ - resolve: (build) => build.createdAt.getTime(), - }), - modelRevision: t.relation("modelRevision"), - errors: t.stringList({ - resolve: (build) => build.errors.filter((e) => e !== ""), - }), - runSeconds: t.exposeFloat("runSeconds"), - }), -}); diff --git a/packages/hub/src/graphql/types/Owner.ts b/packages/hub/src/graphql/types/Owner.ts deleted file mode 100644 index cd6b43ed0a..0000000000 --- a/packages/hub/src/graphql/types/Owner.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { builder } from "../builder"; - -// Note: Owner table is not mapped to GraphQL type. This interface is a proxy for `User` and `Group`. -// common for User and Group -export const Owner = builder.interfaceRef<{ id: string }>("Owner").implement({ - resolveType: (obj) => { - return (obj as any)._owner.type; - }, - fields: (t) => ({ - id: t.exposeID("id"), - slug: t.string(), // implemented on User and Group - }), -}); diff --git a/packages/hub/src/graphql/types/RelativeValuesDefinition.ts b/packages/hub/src/graphql/types/RelativeValuesDefinition.ts deleted file mode 100644 index ab85964354..0000000000 --- a/packages/hub/src/graphql/types/RelativeValuesDefinition.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; -import { - relativeValuesClustersSchema, - relativeValuesItemsSchema, -} from "@/relative-values/types"; - -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { Owner } from "./Owner"; -import { RelativeValuesExport } from "./RelativeValuesExport"; - -const RelativeValuesCluster = builder.simpleObject("RelativeValuesCluster", { - fields: (t) => ({ - id: t.string(), - color: t.string(), - recommendedUnit: t.string({ nullable: true }), - }), -}); - -const RelativeValuesItem = builder.simpleObject("RelativeValuesItem", { - fields: (t) => ({ - id: t.string(), - name: t.string(), - description: t.string(), - clusterId: t.string({ nullable: true }), - }), -}); - -export const RelativeValuesDefinition = builder.prismaNode( - "RelativeValuesDefinition", - { - id: { field: "id" }, - fields: (t) => ({ - slug: t.exposeString("slug"), - // FIXME - copy-pasted from Model.ts - owner: t.field({ - type: Owner, - // TODO - we need to extract fragment data from Owner query and call nestedSelection(...) for optimal performance. - select: { - owner: { - include: { - user: true, - group: true, - }, - }, - }, - resolve: (model) => { - const result = model.owner.user ?? model.owner.group; - if (!result) { - throw new Error("Invalid owner object, missing user or group"); - } - (result as any)["_owner"] = { - type: model.owner.user ? "User" : "Group", - }; - return result; - }, - }), - isEditable: t.boolean({ - authScopes: (definition) => ({ - controlsOwnerId: definition.ownerId, - }), - resolve: () => true, - unauthorizedResolver: () => false, - }), - createdAtTimestamp: t.float({ - resolve: (obj) => obj.createdAt.getTime(), - }), - updatedAtTimestamp: t.float({ - resolve: (obj) => obj.updatedAt.getTime(), - }), - modelExports: t.field({ - type: [RelativeValuesExport], - resolve: async (definition, _, { session }) => { - const models = await prisma.model.findMany({ - where: { - currentRevision: { - relativeValuesExports: { - some: { - definitionId: definition.id, - }, - }, - }, - ...modelWhereHasAccess(session), - }, - }); - - return await prisma.relativeValuesExport.findMany({ - where: { - modelRevisionId: { - in: models - .map((model) => model.currentRevisionId) - .filter((id) => id !== null), - }, - definitionId: definition.id, - }, - }); - }, - }), - currentRevision: t.relation("currentRevision", { - nullable: false, - }), - }), - } -); - -export const RelativeValuesDefinitionRevision = builder.prismaNode( - "RelativeValuesDefinitionRevision", - { - id: { field: "id" }, - fields: (t) => ({ - title: t.exposeString("title"), - items: t.field({ - type: [RelativeValuesItem], - resolve(obj) { - return relativeValuesItemsSchema.parse(obj.items); - }, - }), - clusters: t.field({ - type: [RelativeValuesCluster], - resolve(obj) { - return relativeValuesClustersSchema.parse(obj.clusters); - }, - }), - recommendedUnit: t.exposeString("recommendedUnit", { nullable: true }), - }), - } -); - -export const RelativeValuesDefinitionConnection = builder.connectionObject({ - type: RelativeValuesDefinition, - name: "RelativeValuesDefinitionConnection", -}); - -export const relativeValuesDefinitionConnectionHelpers = - prismaConnectionHelpers(builder, "RelativeValuesDefinition", { - cursor: "id", - }); diff --git a/packages/hub/src/graphql/types/Searchable.ts b/packages/hub/src/graphql/types/Searchable.ts deleted file mode 100644 index 23b905ec9c..0000000000 --- a/packages/hub/src/graphql/types/Searchable.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - groupRoute, - modelRoute, - relativeValuesRoute, - userRoute, -} from "@/routes"; - -import { builder } from "../builder"; -import { Group } from "./Group"; -import { Model } from "./Model"; -import { RelativeValuesDefinition } from "./RelativeValuesDefinition"; -import { User } from "./User"; - -type Tag = "Model" | "RelativeValuesDefinition" | "User" | "Group"; - -function tagged(obj: any, tag: Tag) { - obj._searchableTag = tag; - return obj; -} - -const SearchableObject = builder.unionType("SearchableObject", { - types: [Model, RelativeValuesDefinition, User, Group], - resolveType: (wrappedObj) => { - const tag: Tag = (wrappedObj as any)._searchableTag; - switch (tag) { - case "Model": - return Model; - case "RelativeValuesDefinition": - return RelativeValuesDefinition; - case "User": - return User; - case "Group": - return Group; - default: - throw new Error("Incorrectly tagged object"); - } - }, -}); - -export const Searchable = builder.prismaNode("Searchable", { - id: { field: "id" }, - fields: (t) => ({ - link: t.string({ - select: { - model: { - select: { - owner: true, - slug: true, - }, - }, - definition: { - select: { owner: true, slug: true }, - }, - user: { - select: { asOwner: true }, - }, - group: { - select: { asOwner: true }, - }, - }, - resolve: (object) => { - // TODO - should be a field on object types - switch (true) { - case !!object.model: - return modelRoute({ - owner: object.model.owner.slug, - slug: object.model.slug, - }); - case !!object.definition: - return relativeValuesRoute({ - owner: object.definition.owner.slug, - slug: object.definition.slug, - }); - case !!object.user?.asOwner: - return userRoute({ username: object.user.asOwner.slug }); - case !!object.group: - return groupRoute({ slug: object.group.asOwner.slug }); - default: - throw new Error("Invalid Searchable record"); - } - }, - }), - object: t.field({ - type: SearchableObject, - select: { - // TODO: queryFromInfo, https://github.com/hayes/pothos/discussions/656#discussioncomment-4823928 - model: true, - definition: true, - user: true, - group: true, - }, - resolve: (object) => { - switch (true) { - case !!object.model: - return tagged(object.model, "Model"); - case !!object.definitionId: - return tagged(object.definition, "RelativeValuesDefinition"); - case !!object.userId: - return tagged(object.user, "User"); - case !!object.groupId: - return tagged(object.group, "Group"); - default: - throw new Error("Invalid Searchable record"); - } - }, - }), - }), -}); diff --git a/packages/hub/src/graphql/types/SquiggleSnippet.ts b/packages/hub/src/graphql/types/SquiggleSnippet.ts deleted file mode 100644 index c2b2cc49da..0000000000 --- a/packages/hub/src/graphql/types/SquiggleSnippet.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { builder } from "@/graphql/builder"; - -export const SquiggleSnippet = builder.prismaNode("SquiggleSnippet", { - id: { field: "id" }, - fields: (t) => ({ - code: t.exposeString("code"), - version: t.exposeString("version"), - seed: t.exposeString("seed"), - autorunMode: t.exposeBoolean("autorunMode", { nullable: true }), - sampleCount: t.exposeInt("sampleCount", { nullable: true }), - xyPointLength: t.exposeInt("xyPointLength", { nullable: true }), - }), -}); diff --git a/packages/hub/src/graphql/types/User.ts b/packages/hub/src/graphql/types/User.ts deleted file mode 100644 index 9461ca5a8c..0000000000 --- a/packages/hub/src/graphql/types/User.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { builder } from "../builder"; -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { isRootUser } from "../helpers/userHelpers"; -import { GroupConnection, groupFromMembershipConnectionHelpers } from "./Group"; -import { ModelConnection, modelConnectionHelpers } from "./Model"; -import { Owner } from "./Owner"; -import { - RelativeValuesDefinitionConnection, - relativeValuesDefinitionConnectionHelpers, -} from "./RelativeValuesDefinition"; -import { VariableConnection, variableConnectionHelpers } from "./Variable"; - -export const User = builder.prismaNode("User", { - id: { field: "id" }, - interfaces: [Owner], - fields: (t) => ({ - slug: t.string({ - select: { asOwner: true }, - resolve(user) { - if (!user.asOwner) { - throw new Error("User has no username"); - } - return user.asOwner.slug; - }, - }), - // legacy, alias for user.slug - username: t.string({ - select: { asOwner: true }, - resolve(user) { - if (!user.asOwner) { - throw new Error("User has no username"); - } - return user.asOwner.slug; - }, - }), - // models are stored on owner.models, wo we have to use indirect relation (https://pothos-graphql.dev/docs/plugins/prisma#indirect-relations-as-connections) - // See also: Group.models field. - models: t.connection( - { - type: modelConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - models: { - ...modelConnectionHelpers.getQuery(args, ctx, nestedSelection), - where: modelWhereHasAccess(ctx.session), - orderBy: { updatedAt: "desc" }, - }, - }, - }, - }), - resolve: (user, args, ctx) => - modelConnectionHelpers.resolve(user.asOwner?.models ?? [], args, ctx), - }, - ModelConnection - ), - variables: t.connection( - { - type: variableConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - models: { - where: modelWhereHasAccess(ctx.session), - select: { - variables: variableConnectionHelpers.getQuery( - args, - ctx, - nestedSelection - ), - }, - }, - }, - }, - }), - resolve: (user, args, ctx) => { - const variables = - user.asOwner?.models.map((model) => model.variables ?? []).flat() ?? - []; - return variableConnectionHelpers.resolve(variables, args, ctx); - }, - }, - VariableConnection - ), - relativeValuesDefinitions: t.connection( - { - type: relativeValuesDefinitionConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - relativeValuesDefinitions: { - ...relativeValuesDefinitionConnectionHelpers.getQuery( - args, - ctx, - nestedSelection - ), - orderBy: { updatedAt: "desc" }, - }, - }, - }, - }), - resolve: (user, args, ctx) => - relativeValuesDefinitionConnectionHelpers.resolve( - user.asOwner?.relativeValuesDefinitions ?? [], - args, - ctx - ), - }, - RelativeValuesDefinitionConnection - ), - groups: t.connection( - { - type: groupFromMembershipConnectionHelpers.ref, - - select: (args, ctx, nestedSelection) => ({ - memberships: groupFromMembershipConnectionHelpers.getQuery( - args, - ctx, - nestedSelection - ), - }), - resolve: (user, args, ctx) => - groupFromMembershipConnectionHelpers.resolve( - user.memberships, - args, - ctx - ), - }, - GroupConnection - ), - isRoot: t.boolean({ - authScopes: async (user, _, { session }) => { - return !!( - user.emailVerified && - user.email && - user.email === session?.user.email - ); - }, - resolve: async (user) => isRootUser(user), - }), - }), -}); diff --git a/packages/hub/src/graphql/types/Variable.ts b/packages/hub/src/graphql/types/Variable.ts deleted file mode 100644 index 496c6dcfcf..0000000000 --- a/packages/hub/src/graphql/types/Variable.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; - -import { builder } from "@/graphql/builder"; - -import { Owner } from "./Owner"; -import { VariableRevisionConnection } from "./VariableRevision"; - -export const Variable = builder.prismaNode("Variable", { - id: { field: "id" }, - - include: { model: true }, - authScopes: (variable) => { - if (!variable.model.isPrivate) { - return true; - } - return { controlsOwnerId: variable.model.ownerId }; - }, - - fields: (t) => ({ - variableName: t.exposeString("variableName"), - model: t.relation("model"), - - owner: t.field({ - type: Owner, - select: { - model: { - select: { - owner: { - select: { - user: true, - group: true, - }, - }, - }, - }, - }, - resolve: async ({ model }) => { - const result = model.owner.user ?? model.owner.group; - if (!result) { - throw new Error("Invalid owner object, missing user or group"); - } - (result as any)["_owner"] = { - type: model.owner.user ? "User" : "Group", - }; - return result; - }, - }), - revisions: t.relatedConnection( - "revisions", - { - cursor: "id", - query: () => ({ - orderBy: { - modelRevision: { - createdAt: "desc", - }, - }, - }), - }, - VariableRevisionConnection - ), - currentRevision: t.relation("currentRevision", { - nullable: true, - }), - }), -}); - -export const VariableConnection = builder.connectionObject({ - type: Variable, - name: "VariableConnection", -}); - -export const variableConnectionHelpers = prismaConnectionHelpers( - builder, - "Variable", - { cursor: "id" } -); diff --git a/packages/hub/src/graphql/types/VariableRevision.ts b/packages/hub/src/graphql/types/VariableRevision.ts deleted file mode 100644 index d751cc5d11..0000000000 --- a/packages/hub/src/graphql/types/VariableRevision.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -export const VariableRevision = builder.prismaNode("VariableRevision", { - id: { field: "id" }, - - include: { - modelRevision: { - include: { model: true }, - }, - }, - authScopes: (variableRevision) => { - if (!variableRevision.modelRevision.model.isPrivate) { - return true; - } - return { controlsOwnerId: variableRevision.modelRevision.model.ownerId }; - }, - - fields: (t) => ({ - modelRevision: t.relation("modelRevision"), - variableName: t.exposeString("variableName"), - variableType: t.exposeString("variableType"), - docstring: t.exposeString("docstring"), - title: t.exposeString("title", { nullable: true }), - variable: t.relation("variable"), - }), -}); - -export const variableRevisionConnectionHelpers = prismaConnectionHelpers( - builder, - "VariableRevision", - { cursor: "id" } -); - -export const VariableRevisionConnection = builder.connectionObject({ - type: VariableRevision, - name: "VariableRevisionConnection", -}); - -export type VariableRevisionInput = { - title?: string; - variableName: string; - variableType?: string; - docstring?: string; -}; - -export async function createVariableRevision( - modelId: string, - revisionId: string, - variableData: VariableRevisionInput -) { - const variable = await prisma.variable.findFirst({ - where: { - modelId: modelId, - variableName: variableData.variableName, - }, - }); - - let variableId: string; - if (!variable) { - const createdVariable = await prisma.variable.create({ - data: { - model: { connect: { id: modelId } }, - variableName: variableData.variableName, - }, - }); - variableId = createdVariable.id; - } else { - variableId = variable.id; - } - - const createdVariableRevision = await prisma.variableRevision.create({ - data: { - variableName: variableData.variableName, - variable: { connect: { id: variableId } }, - modelRevision: { connect: { id: revisionId } }, - variableType: variableData.variableType, - title: variableData.title, - docstring: variableData.docstring, - }, - }); - - await prisma.variable.update({ - where: { id: variableId }, - data: { - currentRevisionId: createdVariableRevision.id, - }, - }); -} diff --git a/packages/hub/src/graphql/utils.ts b/packages/hub/src/graphql/utils.ts deleted file mode 100644 index e0df031923..0000000000 --- a/packages/hub/src/graphql/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { decodeGlobalID } from "@pothos/plugin-relay"; -import { ValidationOptions } from "@pothos/plugin-validation"; - -export const validateSlug: ValidationOptions = { - regex: [/^\w[\w\-]*$/, { message: "Must be alphanumerical" }], -}; - -export function decodeGlobalIdWithTypename( - id: string, - typenames: string | string[] // TODO - union based on Pothos types? -) { - // wrapper around decodeGlobalID from Pothos Relay Plugin - const { typename: decodedTypename, id: decodedId } = decodeGlobalID(id); - - if (typeof typenames === "string") { - typenames = [typenames]; - } - if (!typenames.includes(decodedTypename)) { - throw new Error( - `Expected ${typenames.join("|")} id, got: ${decodedTypename}` - ); - } - - return decodedId; -} diff --git a/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts new file mode 100644 index 0000000000..7a0b223ee7 --- /dev/null +++ b/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts @@ -0,0 +1,62 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { getMyMembership } from "@/groups/helpers"; +import { groupMembersRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getSessionOrRedirect } from "@/users/auth"; + +import { validateReusableGroupInviteToken } from "../data/helpers"; + +export const acceptReusableGroupInviteTokenAction = actionClient + .schema( + z.object({ + groupSlug: zSlug, + inviteToken: z.string(), + }) + ) + .action(async ({ parsedInput: input }) => { + const session = await getSessionOrRedirect(); + + if (!(await validateReusableGroupInviteToken(input))) { + throw new ActionError("Invalid token"); + } + + const group = await prisma.group.findFirstOrThrow({ + select: { + id: true, + }, + where: { + asOwner: { slug: input.groupSlug }, + }, + }); + + const myMembership = await getMyMembership({ + groupSlug: input.groupSlug, + }); + if (myMembership) { + throw new ActionError("You're already a member of this group"); + } + + await prisma.userGroupMembership.create({ + data: { + group: { + connect: { + id: group.id, + }, + }, + user: { + connect: { + email: session.user.email, + }, + }, + role: "Member", + }, + }); + + revalidatePath(groupMembersRoute({ slug: input.groupSlug })); + }); diff --git a/packages/hub/src/groups/actions/addUserToGroupAction.ts b/packages/hub/src/groups/actions/addUserToGroupAction.ts new file mode 100644 index 0000000000..0f46b3707b --- /dev/null +++ b/packages/hub/src/groups/actions/addUserToGroupAction.ts @@ -0,0 +1,110 @@ +"use server"; + +import { MembershipRole } from "@prisma/client"; +import { z } from "zod"; + +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getSessionOrRedirect } from "@/users/auth"; + +import { + GroupMemberDTO, + membershipSelect, + membershipToDTO, +} from "../data/members"; + +export const addUserToGroupAction = actionClient + .schema( + z.object({ + group: zSlug, + username: zSlug, + role: z.enum( + Object.keys(MembershipRole) as [keyof typeof MembershipRole] + ), + }) + ) + .action(async ({ parsedInput: input }): Promise => { + const session = await getSessionOrRedirect(); + + const membership = await prisma.$transaction(async (tx) => { + const groupOwner = await tx.owner.findUnique({ + where: { + slug: input.group, + }, + }); + if (!groupOwner) { + throw new ActionError(`Group ${input.group} not found`); + } + + const requestedUser = await tx.user.findFirst({ + where: { + asOwner: { + slug: input.username, + }, + }, + }); + + if (!requestedUser) { + throw new ActionError(`User ${input.username} not found`); + } + + // We perform all checks one by one because that allows more precise error reporting. + // (It would be possible to check everything in one big query with clever nested `connect` checks.) + const isAdmin = await tx.group.count({ + where: { + ownerId: groupOwner.id, + memberships: { + some: { + user: { email: session.user.email }, + role: "Admin", + }, + }, + }, + }); + if (!isAdmin) { + throw new ActionError(`You're not an admin of ${input.group} group`); + } + + const alreadyAMember = await tx.group.count({ + where: { + ownerId: groupOwner.id, + memberships: { + some: { userId: requestedUser.id }, + }, + }, + }); + if (alreadyAMember) { + throw new ActionError( + `${input.username} is already a member of ${input.group}` + ); + } + + // Cancel all pending invites + await tx.groupInvite.updateMany({ + where: { + userId: requestedUser.id, + groupId: groupOwner.id, + status: "Pending", + }, + data: { + status: "Canceled", + }, + }); + + return await tx.userGroupMembership.create({ + data: { + user: { + connect: { id: requestedUser.id }, + }, + group: { + connect: { ownerId: groupOwner.id }, + }, + role: input.role, + }, + select: membershipSelect, + }); + }); + + return membershipToDTO(membership); + }); diff --git a/packages/hub/src/groups/actions/createGroupAction.ts b/packages/hub/src/groups/actions/createGroupAction.ts new file mode 100644 index 0000000000..fe054162c7 --- /dev/null +++ b/packages/hub/src/groups/actions/createGroupAction.ts @@ -0,0 +1,56 @@ +"use server"; +import { z } from "zod"; + +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { indexGroupId } from "@/search/helpers"; +import { getSessionOrRedirect } from "@/users/auth"; + +const schema = z.object({ + slug: zSlug, +}); + +export const createGroupAction = actionClient + .schema(schema) + .action(async ({ parsedInput: input }): Promise<{ slug: string }> => { + const session = await getSessionOrRedirect(); + + const user = await prisma.user.findUniqueOrThrow({ + where: { email: session.user.email }, + }); + + const group = await failValidationOnConstraint( + () => + prisma.group.create({ + data: { + asOwner: { + create: { + slug: input.slug, + }, + }, + memberships: { + create: [{ userId: user.id, role: "Admin" }], + }, + }, + select: { id: true }, + }), + { + schema, + handlers: [ + { + constraint: ["slug"], + input: "slug", + error: `Group ${input.slug} already exists`, + }, + ], + } + ); + + await indexGroupId(group.id); + + return { slug: input.slug }; + }); diff --git a/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts new file mode 100644 index 0000000000..65428573a9 --- /dev/null +++ b/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts @@ -0,0 +1,59 @@ +"use server"; +import crypto from "crypto"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { groupMembersRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; + +import { loadMyMembership } from "../data/members"; + +/* + * Create or replace a reusable invite token for a group, available as + * \`reusableInviteToken\` field on group object. + * + * You must be an admin of the group to call this mutation. Previous invite + * token, if it existed, will stop working. + */ +export const createReusableGroupInviteTokenAction = actionClient + .schema( + z.object({ + slug: zSlug, + }) + ) + .action(async ({ parsedInput: input }): Promise => { + const myMembership = await loadMyMembership({ groupSlug: input.slug }); + if (!myMembership) { + throw new ActionError("You're not a member of this group"); + } + if (myMembership.role !== "Admin") { + throw new ActionError( + "Only group admins can create reusable invite tokens" + ); + } + + const group = await prisma.group.findFirstOrThrow({ + where: { + asOwner: { + slug: input.slug, + }, + }, + }); + + const token = crypto.randomBytes(30).toString("hex"); + + await prisma.group.update({ + where: { + id: group.id, + }, + data: { + // old token will be overwritten, that's fine + reusableInviteToken: token, + }, + select: { id: true }, + }); + + revalidatePath(groupMembersRoute({ slug: input.slug })); + }); diff --git a/packages/hub/src/groups/actions/deleteMembershipAction.ts b/packages/hub/src/groups/actions/deleteMembershipAction.ts new file mode 100644 index 0000000000..ead9cbd5be --- /dev/null +++ b/packages/hub/src/groups/actions/deleteMembershipAction.ts @@ -0,0 +1,72 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { getMembership, getMyMembership } from "@/groups/helpers"; +import { groupMembersRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getSessionOrRedirect } from "@/users/auth"; + +import { groupHasAdminsBesidesUser } from "../data/helpers"; + +export const deleteMembershipAction = actionClient + .schema( + z.object({ + group: zSlug, + username: zSlug, + }) + ) + .action(async ({ parsedInput: input }): Promise<"ok"> => { + const session = await getSessionOrRedirect(); + + // somewhat repetitive compared to `updateMembershipRoleAction`, but with slightly different error messages + const myMembership = await getMyMembership({ + groupSlug: input.group, + }); + + if (!myMembership) { + throw new ActionError("You're not a member of this group"); + } + + if ( + input.username !== session.user.username && + myMembership.role !== "Admin" + ) { + throw new ActionError("Only admins can delete other members"); + } + + const membershipToDelete = await getMembership({ + groupSlug: input.group, + userSlug: input.username, + }); + + if (!membershipToDelete) { + throw new ActionError( + `${input.username} is not a member of ${input.group}` + ); + } + + if ( + !(await groupHasAdminsBesidesUser({ + groupSlug: input.group, + userSlug: input.username, + })) + ) { + throw new ActionError( + `Can't delete, ${input.username} is the last admin of ${input.group}` + ); + } + + await prisma.userGroupMembership.delete({ + where: { + id: membershipToDelete.id, + }, + }); + + revalidatePath(groupMembersRoute({ slug: input.group })); + + return "ok"; + }); diff --git a/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts new file mode 100644 index 0000000000..550f0a7b3e --- /dev/null +++ b/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts @@ -0,0 +1,44 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { groupMembersRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; + +import { loadMyMembership } from "../data/members"; + +// Disable a reusable invite token for a group. +export const deleteReusableGroupInviteTokenAction = actionClient + .schema( + z.object({ + slug: zSlug, + }) + ) + .action(async ({ parsedInput: input }): Promise => { + const myMembership = await loadMyMembership({ groupSlug: input.slug }); + if (!myMembership) { + throw new ActionError("Not a member of this group"); + } + if (myMembership.role !== "Admin") { + throw new ActionError( + "Only group admins can delete reusable invite tokens" + ); + } + + const group = await prisma.group.findFirstOrThrow({ + where: { + asOwner: { + slug: input.slug, + }, + }, + }); + + await prisma.group.update({ + where: { id: group.id }, + data: { reusableInviteToken: null }, + }); + + revalidatePath(groupMembersRoute({ slug: input.slug })); + }); diff --git a/packages/hub/src/groups/actions/updateMembershipRoleAction.ts b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts new file mode 100644 index 0000000000..c0c75d0dab --- /dev/null +++ b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts @@ -0,0 +1,77 @@ +"use server"; + +import { MembershipRole } from "@prisma/client"; +import { z } from "zod"; + +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getSessionOrRedirect } from "@/users/auth"; + +import { groupHasAdminsBesidesUser } from "../data/helpers"; +import { + loadMembership, + loadMyMembership, + membershipSelect, + membershipToDTO, +} from "../data/members"; + +export const updateMembershipRoleAction = actionClient + .schema( + z.object({ + group: zSlug, + user: zSlug, + role: z.enum( + Object.keys(MembershipRole) as [keyof typeof MembershipRole] + ), + }) + ) + .action(async ({ parsedInput: input }) => { + const session = await getSessionOrRedirect(); + + // somewhat repetitive compared to `deleteMembershipAction`, but with slightly different error messages + + const myMembership = await loadMyMembership({ + groupSlug: input.group, + }); + + if (!myMembership) { + throw new ActionError("You're not a member of this group"); + } + + if (input.user !== session.user.username && myMembership.role !== "Admin") { + throw new ActionError("Only admins can update other members roles"); + } + + const membershipToUpdate = await loadMembership({ + groupSlug: input.group, + userSlug: input.user, + }); + + if (!membershipToUpdate) { + throw new ActionError(`${input.user} is not a member of ${input.group}`); + } + + if (membershipToUpdate.role === input.role) { + return membershipToUpdate; // nothing to do + } + + if ( + !(await groupHasAdminsBesidesUser({ + groupSlug: input.group, + userSlug: input.user, + })) + ) { + throw new ActionError( + `Can't change the role, ${input.user} is the last admin of ${input.group}` + ); + } + + const updatedMembership = await prisma.userGroupMembership.update({ + where: { id: membershipToUpdate.id }, + data: { role: input.role }, + select: membershipSelect, + }); + + return membershipToDTO(updatedMembership); + }); diff --git a/packages/hub/src/groups/components/GroupCard.tsx b/packages/hub/src/groups/components/GroupCard.tsx index a18313a7fb..5330178fad 100644 --- a/packages/hub/src/groups/components/GroupCard.tsx +++ b/packages/hub/src/groups/components/GroupCard.tsx @@ -1,27 +1,14 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; -import { groupRoute } from "@/routes"; - -import { GroupCard$key } from "@/__generated__/GroupCard.graphql"; - -const Fragment = graphql` - fragment GroupCard on Group { - id - slug - updatedAtTimestamp - } -`; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { groupRoute } from "@/lib/routes"; type Props = { - groupRef: GroupCard$key; + group: GroupCardDTO; }; -export const GroupCard: FC = ({ groupRef }) => { - const group = useFragment(Fragment, groupRef); - +export const GroupCard: FC = ({ group }) => { return ( = ({ groupRef }) => { })} showOwner={false} slug={group.slug} - menuItems={} + menuItems={} /> ); }; diff --git a/packages/hub/src/groups/components/GroupList.tsx b/packages/hub/src/groups/components/GroupList.tsx index 96a95ebbac..9db4797dc0 100644 --- a/packages/hub/src/groups/components/GroupList.tsx +++ b/packages/hub/src/groups/components/GroupList.tsx @@ -1,43 +1,28 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { LoadMore } from "@/components/LoadMore"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; import { GroupCard } from "./GroupCard"; -import { GroupList$key } from "@/__generated__/GroupList.graphql"; - -const Fragment = graphql` - fragment GroupList on GroupConnection { - edges { - node { - id - ...GroupCard - } - } - pageInfo { - hasNextPage - } - } -`; - type Props = { - connectionRef: GroupList$key; - loadNext(count: number): unknown; + page: Paginated; }; -export const GroupList: FC = ({ connectionRef, loadNext }) => { - const connection = useFragment(Fragment, connectionRef); +export const GroupList: FC = ({ page: initialPage }) => { + const page = usePaginator(initialPage); return (
- {connection.edges.map((edge) => ( - + {page.items.map((group) => ( + ))}
- {connection.pageInfo.hasNextPage && } + {page.loadNext && }
); }; diff --git a/packages/hub/src/groups/data/groupCards.ts b/packages/hub/src/groups/data/groupCards.ts new file mode 100644 index 0000000000..64c39b261a --- /dev/null +++ b/packages/hub/src/groups/data/groupCards.ts @@ -0,0 +1,109 @@ +import { Prisma } from "@prisma/client"; + +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; + +const select = { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + updatedAt: true, +} satisfies Prisma.GroupSelect; + +type DbGroupCard = NonNullable< + Awaited>> +>; + +export type GroupCardDTO = { + id: string; + slug: string; + updatedAt: Date; +}; + +export function toDTO(dbGroup: DbGroupCard): GroupCardDTO { + return { + id: dbGroup.id, + slug: dbGroup.asOwner.slug, + updatedAt: dbGroup.updatedAt, + }; +} + +export async function loadGroupCards( + params: { + username?: string; + cursor?: string; + limit?: number; + } = {} +): Promise> { + const limit = params.limit ?? 20; + + const dbGroups = await prisma.group.findMany({ + select: select, + orderBy: { updatedAt: "desc" }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: { + memberships: { + some: { + user: { + asOwner: { + slug: params.username, + }, + }, + }, + }, + }, + take: limit + 1, + }); + + const groups = dbGroups.map(toDTO); + + const nextCursor = groups[groups.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadGroupCards({ ...params, cursor: nextCursor, limit }); + } + + return { + items: groups.slice(0, limit), + loadMore: groups.length > limit ? loadMore : undefined, + }; +} + +export async function loadGroupCard( + groupSlug: string +): Promise { + const group = await prisma.group.findFirst({ + select, + where: { asOwner: { slug: groupSlug } }, + }); + return group ? toDTO(group) : null; +} + +export async function getMyGroup( + groupSlug: string +): Promise { + const session = await auth(); + const userId = session?.user.id; + if (!userId) { + return null; + } + + const group = await prisma.group.findFirst({ + select, + where: { + asOwner: { slug: groupSlug }, + memberships: { + some: { userId }, + }, + }, + }); + if (!group) { + return null; + } + return toDTO(group); +} diff --git a/packages/hub/src/groups/data/helpers.ts b/packages/hub/src/groups/data/helpers.ts new file mode 100644 index 0000000000..80bd4ebcd7 --- /dev/null +++ b/packages/hub/src/groups/data/helpers.ts @@ -0,0 +1,59 @@ +import { prisma } from "@/lib/server/prisma"; +import { getSessionUserOrRedirect } from "@/users/auth"; + +import { getMyGroup } from "./groupCards"; + +export async function hasGroupMembership(groupSlug: string): Promise { + // TODO - could be optimized + return !!(await getMyGroup(groupSlug)); +} + +export async function validateReusableGroupInviteToken(input: { + groupSlug: string; + inviteToken: string; +}) { + await getSessionUserOrRedirect(); + + const group = await prisma.group.findFirstOrThrow({ + where: { + asOwner: { + slug: input.groupSlug, + }, + }, + }); + + if (group.reusableInviteToken !== input.inviteToken) { + return false; + } + + return true; +} + +// also returns true if user is not an admin +export async function groupHasAdminsBesidesUser({ + groupSlug, + userSlug, +}: { + groupSlug: string; + userSlug: string; +}) { + return Boolean( + await prisma.userGroupMembership.count({ + where: { + group: { + asOwner: { + slug: groupSlug, + }, + }, + NOT: { + user: { + asOwner: { + slug: userSlug, + }, + }, + }, + role: "Admin", + }, + }) + ); +} diff --git a/packages/hub/src/groups/data/members.ts b/packages/hub/src/groups/data/members.ts new file mode 100644 index 0000000000..bf5ab74eff --- /dev/null +++ b/packages/hub/src/groups/data/members.ts @@ -0,0 +1,134 @@ +import { MembershipRole, Prisma } from "@prisma/client"; + +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; + +export type GroupMemberDTO = { + id: string; + role: MembershipRole; + user: { + slug: string; + }; +}; + +const select = { + id: true, + role: true, + user: { + select: { + asOwner: { + select: { slug: true }, + }, + }, + }, +} satisfies Prisma.UserGroupMembershipSelect; + +export const membershipSelect = select; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.userGroupMembership.findFirst<{ select: typeof select }> + > + > +>; + +export function membershipToDTO(row: Row): GroupMemberDTO { + return { + id: row.id, + role: row.role, + // guaranteed by the query + user: { slug: row.user.asOwner!.slug }, + }; +} + +export async function loadGroupMembers(params: { + groupSlug: string; + cursor?: string; + limit?: number; +}): Promise> { + const limit = params.limit ?? 20; + + const rows = await prisma.userGroupMembership.findMany({ + where: { + group: { + asOwner: { + slug: params.groupSlug, + }, + }, + user: { + asOwner: { + isNot: null, + }, + }, + }, + select, + orderBy: { createdAt: "asc" }, + take: limit + 1, + }); + + const members = rows.map(membershipToDTO); + + const nextCursor = members[members.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadGroupMembers({ ...params, cursor: nextCursor, limit }); + } + + return { + items: members.slice(0, limit), + loadMore: members.length > limit ? loadMore : undefined, + }; +} + +export async function loadMyMembership(params: { + groupSlug: string; +}): Promise { + const session = await auth(); + + const userId = session?.user.id; + if (!userId) { + return null; + } + + const membership = await prisma.userGroupMembership.findFirst({ + where: { + group: { asOwner: { slug: params.groupSlug } }, + user: { id: userId }, + }, + select, + }); + return membership ? membershipToDTO(membership) : null; +} + +export async function loadMembership(params: { + groupSlug: string; + userSlug: string; +}): Promise { + const membership = await prisma.userGroupMembership.findFirst({ + where: { + group: { asOwner: { slug: params.groupSlug } }, + user: { asOwner: { slug: params.userSlug } }, + }, + select, + }); + return membership ? membershipToDTO(membership) : null; +} + +export async function loadReusableInviteToken(params: { + groupSlug: string; +}): Promise { + const myMembership = await loadMyMembership({ groupSlug: params.groupSlug }); + if (myMembership?.role !== "Admin") { + throw new Error("Not an admin"); + } + + const group = await prisma.group.findFirst({ + where: { asOwner: { slug: params.groupSlug } }, + select: { reusableInviteToken: true }, + }); + + return group?.reusableInviteToken ?? null; +} diff --git a/packages/hub/src/groups/helpers.ts b/packages/hub/src/groups/helpers.ts new file mode 100644 index 0000000000..ef4d26d336 --- /dev/null +++ b/packages/hub/src/groups/helpers.ts @@ -0,0 +1,46 @@ +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; + +import { isSignedIn } from "../users/auth"; + +export async function getMembership({ + groupSlug, + userSlug, +}: { + groupSlug: string; + userSlug: string; +}) { + const membership = await prisma.userGroupMembership.findFirst({ + where: { + user: { + asOwner: { + slug: userSlug, + }, + }, + group: { + asOwner: { + slug: groupSlug, + }, + }, + }, + }); + return membership; +} + +export async function getMyMembership({ groupSlug }: { groupSlug: string }) { + const session = await auth(); + if (!isSignedIn(session)) { + return null; + } + const myMembership = await prisma.userGroupMembership.findFirst({ + where: { + userId: session.user.id, + group: { + asOwner: { + slug: groupSlug, + }, + }, + }, + }); + return myMembership; +} diff --git a/packages/hub/src/hooks/useAsyncMutation.ts b/packages/hub/src/hooks/useAsyncMutation.ts deleted file mode 100644 index 2b5c0a0436..0000000000 --- a/packages/hub/src/hooks/useAsyncMutation.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { useState } from "react"; -import { useMutation, UseMutationConfig } from "react-relay"; -import { GraphQLTaggedNode, MutationParameters } from "relay-runtime"; - -import { useToast } from "@quri/ui"; - -export type CommonMutationParameters = - MutationParameters & { - response: { - readonly result: - | { - readonly __typename: "BaseError"; - readonly message: string; - } - | { - readonly __typename: TTypename; - } - | { - readonly __typename: "%other"; - }; - }; - }; - -type UseAsyncMutationOkResult< - TMutation extends CommonMutationParameters, - TTypename extends string = string, -> = Extract; - -export type UseAsyncMutationAct< - TMutation extends CommonMutationParameters, - TTypename extends string = string, -> = ( - config: Omit, "onCompleted" | "onError"> & { - onCompleted?: ( - okResult: UseAsyncMutationOkResult - ) => void; - } -) => Promise; - -/** - * Like the basic `useMutation` from Relay, this function returns a `[runMutation, isMutationInFlight]` pair. - * But unlike `useMutation`, returned `runMutation` is async and has more convenient `onCompleted` callback (it receives only an expected fragment, unwrapped from the result union). - * Also, all errors will be displayed as notifications automatically. - */ -export function useAsyncMutation< - TMutation extends CommonMutationParameters, - TTypename extends string = string, ->({ - mutation, - confirmation, - expectedTypename, - blockOnSuccess, -}: { - mutation: GraphQLTaggedNode; - confirmation?: string; - expectedTypename: TTypename; - blockOnSuccess?: boolean; // mark mutation as in flight on success; useful if `onCompleted` callback does `router.push` -}) { - const toast = useToast(); - - const [runMutation, inFlight] = useMutation(mutation); - const [wasCompleted, setWasCompleted] = useState(false); - - type OkResult = UseAsyncMutationOkResult; - - const act: UseAsyncMutationAct = (config) => { - return new Promise((resolve, reject) => { - runMutation({ - ...config, - onCompleted(response) { - if (response.result.__typename === expectedTypename) { - if (confirmation !== undefined) { - toast(confirmation, "confirmation"); - } - setWasCompleted(true); - config.onCompleted?.(response.result as OkResult); - resolve(); - } else if ( - // TODO - would `endsWith('Error')` be ok? - // Or can be ask Relay for all types that implement Error interface somehow? - response.result.__typename === "BaseError" || - response.result.__typename === "ValidationError" - ) { - toast( - // This is more cautious than necessary, simple casting to any would be fine too, but it doesn't hurt. - (response.result as { message?: string })?.message ?? - "Internal error", - "error" - ); - reject(); - } else { - toast("Unexpected response type", "error"); - reject(); - } - }, - updater(store, response) { - if (!response) { - return; - } - if (response.result.__typename !== expectedTypename) { - return; - } - config.updater?.(store, response); - }, - onError(e) { - toast(e.toString(), "error"); - resolve(); - }, - }); - }); - }; - - return [act, inFlight || Boolean(blockOnSuccess && wasCompleted)] as const; -} diff --git a/packages/hub/src/hooks/useMutationForm.ts b/packages/hub/src/hooks/useMutationForm.ts deleted file mode 100644 index 6b8ebd83fc..0000000000 --- a/packages/hub/src/hooks/useMutationForm.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { BaseSyntheticEvent, useCallback } from "react"; -import { FieldValues, useForm, UseFormProps } from "react-hook-form"; -import { VariablesOf } from "relay-runtime"; - -import { CommonMutationParameters, useAsyncMutation } from "./useAsyncMutation"; - -/** - * This hook ties together `useForm` and `useAsyncMutation`, which is a very common pattern for forms that submit data to our backend through mutations. - * It forwards some of its parameters to `useForm`, and others to `useAsyncMutation`, and converts form's data to mutation's variables with `formDataToVariables` function. - * - * See also: - * - `` if your form is available through a Dropdown menu - * - `useAsyncMutation` if your mutation doesn't require a form, and for the details on parameters for this function - * - * All generic type parameters to this function default to `never`, so you'll have to set them explicitly to pass type checks. - */ -export function useMutationForm< - FormShape extends FieldValues = never, - TMutation extends CommonMutationParameters = never, - TTypename extends string = never, - // In some cases, you might want to pass extra data to the mutation, which isn't available in `formDataToVariables`. - // See `SaveButton` implementation in `EditSquiggleSnippetModel` for an example where it's needed. - // Longer comment: https://github.com/quantified-uncertainty/squiggle/pull/2498#discussion_r1385665972 - ExtraData extends Record = Record, ->({ - // useForm params - defaultValues, - mode, - // useAsyncMutation params - mutation, - expectedTypename, - blockOnSuccess, - confirmation, - // runMutation params - onCompleted, - // bridge from form to runMutation - formDataToVariables, -}: { - // This is unfortunately not strictly type-safe: if you return extra variables that are not needed for mutation, TypeScript won't complain. - // See also: https://stackoverflow.com/questions/72111571/typescript-exact-return-type-of-function - // This could be solved by converting the return type to generic, but I expect that the lack of partial type parameters in TypeScript - // would get in the way, so I won't even try. - formDataToVariables: ( - data: FormShape, - extraData?: ExtraData - ) => VariablesOf; -} & Pick, "defaultValues" | "mode"> & - Pick< - Parameters>[0], - // I could just pass everything but I'm worried about potential name collisions, and it's easy to whitelist everything necessary. - // We have to list all these when we pass them to `useAsyncMutation`, anyway. - "mutation" | "expectedTypename" | "blockOnSuccess" | "confirmation" - > & - // TODO - with `useAsyncMutation`, submitted data is available because `onCompleted` is usually a closure over it. - // For `useMutationForm`, it'd be useful to pass it to `onCompleted` explicitly, `onCompleted(result, submittedData)`. - Pick< - Parameters>[0]>[0], - "onCompleted" - >) { - const form = useForm({ defaultValues, mode }); - - const [runMutation, inFlight] = useAsyncMutation({ - mutation, - expectedTypename, - blockOnSuccess, - confirmation, - }); - - const onSubmit = useCallback( - (event?: BaseSyntheticEvent, extraData?: ExtraData) => - form.handleSubmit((formData) => { - runMutation({ - variables: formDataToVariables(formData, extraData), - onCompleted, - }); - })(event), - [form, formDataToVariables, onCompleted, runMutation] - ); - - return { form, onSubmit, inFlight }; -} diff --git a/packages/hub/src/hooks/useUsername.ts b/packages/hub/src/hooks/useUsername.ts deleted file mode 100644 index 6efb10083b..0000000000 --- a/packages/hub/src/hooks/useUsername.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useSession } from "next-auth/react"; - -export function useUsername(): string | undefined { - const { data: session } = useSession(); - return session?.user?.username; -} diff --git a/packages/hub/src/lib/README.md b/packages/hub/src/lib/README.md new file mode 100644 index 0000000000..3c9c0d4f16 --- /dev/null +++ b/packages/hub/src/lib/README.md @@ -0,0 +1,7 @@ +Files that are shared between different parts of the codebase. + +`lib/server` is for server-only code. + +`lib/hooks` for common React hooks. + +Other files are shared. diff --git a/packages/hub/src/lib/common.ts b/packages/hub/src/lib/constants.ts similarity index 54% rename from packages/hub/src/lib/common.ts rename to packages/hub/src/lib/constants.ts index d3e5142866..24c081347c 100644 --- a/packages/hub/src/lib/common.ts +++ b/packages/hub/src/lib/constants.ts @@ -1,13 +1,23 @@ // for format() from date-fns export const commonDateFormat = "MMM d yyyy, H:mm"; +// Footer links, etc. export const DISCORD_URL = "https://discord.gg/nsTnQTgtG6"; export const GITHUB_URL = "https://github.com/quantified-uncertainty/squiggle"; export const GITHUB_DISCUSSION_URL = "https://github.com/quantified-uncertainty/squiggle/discussions"; -export const GRAPHQL_URL = "https://squigglehub.org/api/graphql"; export const NEWSLETTER_URL = "https://quri.substack.com/t/squiggle"; export const QURI_DONATE_URL = "https://quantifieduncertainty.org/donate"; export const SQUIGGLE_DOCS_URL = "https://www.squiggle-language.com/docs/Api/Dist"; + +// Don't try to destructure this, `const { NEXT_PUBLIC_FOO } = process.env` won't work correctly. +// Note that only `NEXT_PUBLIC_*` vars are affected; others can be used through `process.env.FOO` without issues. +export const VERCEL_URL = process.env["NEXT_PUBLIC_VERCEL_BRANCH_URL"]; + +// Squiggle defaults +export const SAMPLE_COUNT_DEFAULT = 1000; +export const XY_POINT_LENGTH_DEFAULT = 1000; + +export const DEFAULT_SEED = "DEFAULT_SEED"; diff --git a/packages/hub/src/lib/graphqlHelpers.ts b/packages/hub/src/lib/graphqlHelpers.ts deleted file mode 100644 index 83486d26f8..0000000000 --- a/packages/hub/src/lib/graphqlHelpers.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { notFound } from "next/navigation"; - -type ErrorUnionNode = - | { - readonly __typename: "BaseError"; - readonly message: string; - } - | { - readonly __typename: "NotFoundError"; - readonly message: string; - } - | { - readonly __typename: "%other"; - } - | ({ - readonly __typename: OkTypename; - } & Content) - | null; // useful when node is obtained through top-level `node { ... }` - -/* - * This function will render 404 on NotFoundError and throw on other errors; - * Otherwise, it will extract the value for a given typename. - * - * Note: implementation is not very type-safe. - * Maybe because `OkTypename` can overlap with other `__typename` values? - * I tried to do `OkTypename extends "BaseError" ? never : OkTypename`, but it didn't help. - */ -export function extractFromGraphqlErrorUnion< - OkTypename extends string, - Content, ->(node: ErrorUnionNode, typename: OkTypename) { - if (node === null || node.__typename === "NotFoundError") { - notFound(); - } - if (node.__typename === "BaseError") { - throw new Error( - // somehow Typescript fails to infer this - ( - node as { readonly __typename: "BaseError"; readonly message: string } - ).message - ); - } - if (node.__typename !== typename) { - throw new Error("Unexpected typename"); - } - - return node as { readonly __typename: OkTypename } & Content; -} diff --git a/packages/hub/src/hooks/useAvailableHeight.ts b/packages/hub/src/lib/hooks/useAvailableHeight.ts similarity index 100% rename from packages/hub/src/hooks/useAvailableHeight.ts rename to packages/hub/src/lib/hooks/useAvailableHeight.ts diff --git a/packages/hub/src/hooks/useClientOnlyRender.ts b/packages/hub/src/lib/hooks/useClientOnlyRender.ts similarity index 100% rename from packages/hub/src/hooks/useClientOnlyRender.ts rename to packages/hub/src/lib/hooks/useClientOnlyRender.ts diff --git a/packages/hub/src/lib/hooks/usePaginator.ts b/packages/hub/src/lib/hooks/usePaginator.ts new file mode 100644 index 0000000000..18bf1f5403 --- /dev/null +++ b/packages/hub/src/lib/hooks/usePaginator.ts @@ -0,0 +1,64 @@ +import { useCallback, useState } from "react"; + +import { Paginated } from "@/lib/types"; + +type FullPaginated = { + items: T[]; + // this is intentionally named `loadNext` instead of `loadMore` to avoid confusion + loadNext?: (limit: number) => void; + // Helper functions - if the server action has affected some items, we need to update the state. + // This is similar to Relay's edge directives (https://relay.dev/docs/guided-tour/list-data/updating-connections/), + // but more manual. + append: (item: T) => void; + remove: (compare: (item: T) => boolean) => void; + update: (update: (item: T) => T) => void; +}; + +export function usePaginator(initialPage: Paginated): FullPaginated { + const [{ items, loadMore }, setPage] = useState(initialPage); + + const append = useCallback((newItem: T) => { + setPage(({ items, loadMore }) => ({ + items: [...items, newItem], + loadMore, + })); + }, []); + + const remove = useCallback((compare: (item: T) => boolean) => { + setPage(({ items, loadMore }) => ({ + items: items.filter((i) => !compare(i)), + loadMore, + })); + }, []); + + const update = useCallback((update: (item: T) => T) => { + setPage(({ items, loadMore }) => { + const newItems = { + items: items.map(update), + loadMore, + }; + console.log(newItems); + return newItems; + }); + }, []); + + return { + items, + loadNext: loadMore + ? (limit: number) => { + loadMore(limit).then(({ items: newItems, loadMore: newLoadMore }) => { + // In theory, there should be no duplicates, if `loadMore` is implemented correctly. + // But maybe we should check for duplicate keys and skip them. + // This would require a separate (optional?) `getKey` parameter to this hook. + setPage(({ items }) => ({ + items: [...items, ...newItems], + loadMore: newLoadMore, + })); + }); + } + : undefined, + append, + remove, + update, + }; +} diff --git a/packages/hub/src/lib/hooks/useSafeActionForm.ts b/packages/hub/src/lib/hooks/useSafeActionForm.ts new file mode 100644 index 0000000000..9180c9999b --- /dev/null +++ b/packages/hub/src/lib/hooks/useSafeActionForm.ts @@ -0,0 +1,110 @@ +import { + InferSafeActionFnInput, + InferSafeActionFnResult, +} from "next-safe-action"; +import { HookSafeActionFn, useAction } from "next-safe-action/hooks"; +import { BaseSyntheticEvent, useCallback } from "react"; +import { FieldValues, Path, useForm, UseFormProps } from "react-hook-form"; + +import { useToast } from "@quri/ui"; + +/** + * This hook ties together `useForm` and server actions. + * + * See also: + * - `` if your form is available through a Dropdown menu + * + * All generic type parameters to this function default to `never`, so you'll have to set them explicitly to pass type checks. + */ +export function useSafeActionForm< + FormShape extends FieldValues = never, + const Action extends HookSafeActionFn = never, + ExtraData extends Record = Record, + ActionInput = InferSafeActionFnInput["clientInput"], +>({ + defaultValues, + mode, + action, + onSuccess, + formDataToInput, + blockOnSuccess, +}: { + // This is unfortunately not strictly type-safe: if you return extra variables that are not needed for mutation, TypeScript won't complain. + // See also: https://stackoverflow.com/questions/72111571/typescript-exact-return-type-of-function + // This could be solved by converting the return type to generic, but I expect that the lack of partial type parameters in TypeScript + // would get in the way, so I won't even try. + formDataToInput: (data: FormShape, extraData?: ExtraData) => ActionInput; + action: Action; + onSuccess?: ( + result: NonNullable["data"]> + ) => void | Promise; + blockOnSuccess?: boolean; +} & Pick, "defaultValues" | "mode">) { + const form = useForm({ defaultValues, mode }); + + const toast = useToast(); + + // TODO - use https://github.com/next-safe-action/adapter-react-hook-form + + const { executeAsync, isPending, hasSucceeded } = useAction(action, { + onSuccess: ({ data }) => { + if (data) { + onSuccess?.(data); + } + }, + onError: ({ error }) => { + if (error.serverError) { + toast(String(error.serverError), "error"); + return; + } + + // validation errors? + if (error.validationErrors) { + // TODO - support top-level global validation error + + let hasErrors = false; + for (const [field, fieldErrors] of Object.entries( + error.validationErrors + )) { + if (!fieldErrors || typeof fieldErrors !== "object") { + continue; + } + + if ("_errors" in fieldErrors && Array.isArray(fieldErrors._errors)) { + const fieldError = fieldErrors._errors[0]; + if (fieldError) { + // TODO - check that the field exists in the form + form.setError(field as Path, { + message: String(fieldError), + }); + hasErrors = true; + } + } + } + + if (hasErrors) { + return; + } + } + + toast("Internal error", "error"); + }, + }); + + const onSubmit = useCallback( + (event?: BaseSyntheticEvent, extraData?: ExtraData) => + form.handleSubmit(async (formData) => { + const result = await executeAsync(formDataToInput(formData, extraData)); + if (result?.serverError || result?.validationErrors) { + throw new Error("Action failed"); + } + })(event), + [form, formDataToInput, executeAsync] + ); + + return { + form, + onSubmit, + inFlight: blockOnSuccess ? isPending || hasSucceeded : isPending, + }; +} diff --git a/packages/hub/src/hooks/useUpdateSearchParams.ts b/packages/hub/src/lib/hooks/useUpdateSearchParams.ts similarity index 100% rename from packages/hub/src/hooks/useUpdateSearchParams.ts rename to packages/hub/src/lib/hooks/useUpdateSearchParams.ts diff --git a/packages/hub/src/routes.ts b/packages/hub/src/lib/routes.ts similarity index 89% rename from packages/hub/src/routes.ts rename to packages/hub/src/lib/routes.ts index be49bcbf72..17853192df 100644 --- a/packages/hub/src/routes.ts +++ b/packages/hub/src/lib/routes.ts @@ -89,6 +89,21 @@ export function variableRoute({ return `${modelUrl}/variables/${variableName}`; } +export function variableRevisionRoute({ + owner, + modelSlug, + variableName, + revisionId, +}: { + owner: string; + modelSlug: string; + variableName: string; + revisionId: string; +}) { + const modelUrl = modelRoute({ owner, slug: modelSlug }); + return `${modelUrl}/variables/${variableName}/revisions/${revisionId}`; +} + export function modelViewRoute({ username, slug, @@ -148,6 +163,18 @@ export function userVariablesRoute({ username }: { username: string }) { return `/users/${username}/variables`; } +export function groupsRoute() { + return "/groups"; +} + +export function definitionsRoute() { + return "/definitions"; +} + +export function variablesRoute() { + return "/variables"; +} + export function groupRoute({ slug }: { slug: string }) { return `/groups/${slug}`; } @@ -171,22 +198,6 @@ export function newGroupRoute() { return "/new/group"; } -export function graphqlAPIRoute() { - return graphqlPlaygroundRoute(); -} - -export function graphqlPlaygroundRoute(query?: string) { - const paramsString = - query === undefined - ? "" - : "?" + - new URLSearchParams({ - query, - }).toString(); - - return `/api/graphql${paramsString}`; -} - export function aboutRoute() { return "/about"; } diff --git a/packages/hub/src/lib/server/actionClient.ts b/packages/hub/src/lib/server/actionClient.ts new file mode 100644 index 0000000000..4a3106e92f --- /dev/null +++ b/packages/hub/src/lib/server/actionClient.ts @@ -0,0 +1,68 @@ +import { Prisma } from "@prisma/client"; +import { + createSafeActionClient, + DEFAULT_SERVER_ERROR_MESSAGE, + returnValidationErrors, + ValidationErrors, +} from "next-safe-action"; +import { z } from "zod"; + +export class ActionError extends Error {} + +export const actionClient = createSafeActionClient({ + handleServerError(e) { + console.error("Action error:", e.message); + + if (e instanceof ActionError) { + return e.message; + } + + return DEFAULT_SERVER_ERROR_MESSAGE; + }, +}); + +// Converts Prisma constraint error (usually happens on create operations) to next-safe-action validation error. +// This allows us to attach the error to the form field that caused the error. +// The constraint must be known - if the constraint happens on a field that wasn't provided in the configuration, it will be thrown as a generic server-side error (and hidden in production). +export async function failValidationOnConstraint( + cb: () => Promise, + // This configuration is a bit verbose because it's the most generic form: it supports multiple possible constraints, with multiple handlers. + // A complicated "create" operation could, theoretically, have multiple ways to fail uniqueness, and we'd need to report different fields for each. + { + schema, + handlers, + }: { + schema: S; + handlers: { + // Which field or a combination of fields caused the error. + // For example, if the error happens on a unique constraint on `slug` and `ownerId`, the constraint should be `["slug", "ownerId"]`. + constraint: string[]; + // Action's input field name that will be reported in the validation error. + // Must be a key on the action's input schema. + // Nested keys are not supported. + input: keyof z.input; + // Error message to report. + error: string; + }[]; + } +): Promise { + try { + return await cb(); + } catch (e) { + for (const handler of handlers) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === "P2002" && + Array.isArray(e.meta?.["target"]) && + e.meta?.["target"].join(",") === handler.constraint.join(",") + ) { + returnValidationErrors(schema, { + [handler.input]: { + _errors: [handler.error], + }, + } as ValidationErrors>); + } + } + throw e; + } +} diff --git a/packages/hub/src/auth.ts b/packages/hub/src/lib/server/auth.ts similarity index 79% rename from packages/hub/src/auth.ts rename to packages/hub/src/lib/server/auth.ts index 13c85ae3e6..2036be7e9e 100644 --- a/packages/hub/src/auth.ts +++ b/packages/hub/src/lib/server/auth.ts @@ -5,9 +5,10 @@ import NextAuth, { NextAuthConfig } from "next-auth"; import EmailProvider from "next-auth/providers/email"; import GithubProvider from "next-auth/providers/github"; import { Provider } from "next-auth/providers/index"; +import { cache } from "react"; -import { indexUserId } from "@/graphql/helpers/searchHelpers"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; +import { indexUserId } from "@/search/helpers"; function buildAuthConfig(): NextAuthConfig { const providers: Provider[] = []; @@ -62,4 +63,9 @@ function buildAuthConfig(): NextAuthConfig { return config; } -export const { auth, handlers, signIn, signOut } = NextAuth(buildAuthConfig()); +const nextAuth = NextAuth(buildAuthConfig()); +export const { handlers, signIn, signOut } = nextAuth; + +// current next-auth v5 beta doesn't cache the session, unsure if intentionally +// note: this is React builtin cache, so it's per-request +export const auth = cache(nextAuth.auth); diff --git a/packages/hub/src/lib/server/globalStatistics.ts b/packages/hub/src/lib/server/globalStatistics.ts new file mode 100644 index 0000000000..95fcff7094 --- /dev/null +++ b/packages/hub/src/lib/server/globalStatistics.ts @@ -0,0 +1,14 @@ +import { prisma } from "@/lib/server/prisma"; + +export async function getGlobalStatistics() { + const userCount = await prisma.user.count(); + const modelCount = await prisma.model.count(); + const relativeValuesDefinitionCount = + await prisma.relativeValuesDefinition.count(); + + return { + users: userCount, + models: modelCount, + relativeValuesDefinitions: relativeValuesDefinitionCount, + }; +} diff --git a/packages/hub/src/prisma.ts b/packages/hub/src/lib/server/prisma.ts similarity index 91% rename from packages/hub/src/prisma.ts rename to packages/hub/src/lib/server/prisma.ts index 7d7daad671..2f04ee9e17 100644 --- a/packages/hub/src/prisma.ts +++ b/packages/hub/src/lib/server/prisma.ts @@ -1,7 +1,5 @@ -/* - * TODO - it would be good to `import "server-only"` here, as a precaution, but - * this interferes with `tsx ./src/graphql/print-schema.ts`. - */ +import "server-only"; + import { PrismaClient } from "@prisma/client"; declare global { diff --git a/packages/hub/src/graphql/queries/runSquiggle.ts b/packages/hub/src/lib/server/runSquiggle.ts similarity index 67% rename from packages/hub/src/graphql/queries/runSquiggle.ts rename to packages/hub/src/lib/server/runSquiggle.ts index 7394343a8d..a80e760b40 100644 --- a/packages/hub/src/graphql/queries/runSquiggle.ts +++ b/packages/hub/src/lib/server/runSquiggle.ts @@ -9,12 +9,9 @@ import { SqValue, } from "@quri/squiggle-lang"; -import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/constants"; -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; -import { parseSourceId } from "@/squiggle/components/linker"; - -import { NotFoundError } from "../errors/NotFoundError"; +import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/lib/constants"; +import { prisma } from "@/lib/server/prisma"; +import { parseSourceId } from "@/squiggle/linker"; function getKey(code: string, seed: string): string { return crypto @@ -44,51 +41,6 @@ type SquiggleOutput = { } ); -const SquiggleOutputObj = builder - .interfaceRef("SquiggleOutput") - .implement({ - fields: (t) => ({ - isCached: t.exposeBoolean("isCached"), - }), - }); - -builder.objectType( - builder.objectRef>( - "SquiggleOkOutput" - ), - { - name: "SquiggleOkOutput", - interfaces: [SquiggleOutputObj], - isTypeOf: (value) => (value as SquiggleOutput).isOk, - fields: (t) => ({ - resultJSON: t.string({ - resolve(obj) { - return JSON.stringify(obj.resultJSON); - }, - }), - bindingsJSON: t.string({ - resolve(obj) { - return JSON.stringify(obj.bindingsJSON); - }, - }), - }), - } -); - -builder.objectType( - builder.objectRef>( - "SquiggleErrorOutput" - ), - { - name: "SquiggleErrorOutput", - interfaces: [SquiggleOutputObj], - isTypeOf: (value) => !(value as SquiggleOutput).isOk, - fields: (t) => ({ - errorString: t.exposeString("errorString"), - }), - } -); - export const squiggleLinker: SqLinker = { resolve(name) { return name; @@ -110,14 +62,14 @@ export const squiggleLinker: SqLinker = { }); if (!model) { - throw new NotFoundError(); + throw new Error("Not found"); } const content = model?.currentRevision?.squiggleSnippet; if (content) { return new SqModule({ name: sourceId, code: content.code }); } else { - throw new NotFoundError(); + throw new Error("Not found"); } }, }; @@ -199,17 +151,3 @@ export async function runSquiggleWithCache( return result; } - -builder.queryField("runSquiggle", (t) => - t.field({ - type: SquiggleOutputObj, - args: { - code: t.arg.string({ required: true }), - seed: t.arg.string({ required: false }), - }, - async resolve(_, { code, seed }) { - const result = await runSquiggleWithCache(code, seed || "DEFAULT_SEED"); - return result; - }, - }) -); diff --git a/packages/hub/src/lib/types.ts b/packages/hub/src/lib/types.ts new file mode 100644 index 0000000000..0966b174ea --- /dev/null +++ b/packages/hub/src/lib/types.ts @@ -0,0 +1,4 @@ +export type Paginated = { + items: T[]; + loadMore?: (limit: number) => Promise>; +}; diff --git a/packages/hub/src/lib/zodUtils.ts b/packages/hub/src/lib/zodUtils.ts index 1f6958a75d..c61447b7d6 100644 --- a/packages/hub/src/lib/zodUtils.ts +++ b/packages/hub/src/lib/zodUtils.ts @@ -16,3 +16,11 @@ export const numberInString = z.string().transform((val, ctx) => { } return parsed; }); + +export const zSlug = z.string().regex(/^\w[\w\-]*$/, { + message: "Must be alphanumerical", +}); + +export const zColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, { + message: "Must be a valid hex color", +}); diff --git a/packages/hub/src/migrations/20241012155427_workflow_format.ts b/packages/hub/src/migrations/20241012155427_workflow_format.ts index b3e2daf6c7..36691649c1 100644 --- a/packages/hub/src/migrations/20241012155427_workflow_format.ts +++ b/packages/hub/src/migrations/20241012155427_workflow_format.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; export async function migrate() { const v1Workflows = await prisma.aiWorkflow.findMany({ diff --git a/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts new file mode 100644 index 0000000000..a1615dedcf --- /dev/null +++ b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts @@ -0,0 +1,88 @@ +"use server"; +import { z } from "zod"; + +import { actionClient } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { checkRootUser } from "@/users/auth"; + +// Admin-only query for upgrading model versions +export const adminUpdateModelVersionAction = actionClient + .schema( + z.object({ + modelId: z.string(), + version: z.string(), + }) + ) + .action(async ({ parsedInput: input }) => { + const self = await checkRootUser(); + + const model = await prisma.$transaction(async (tx) => { + let model = await prisma.model.findUniqueOrThrow({ + where: { id: input.modelId }, + include: { + currentRevision: { + include: { + squiggleSnippet: true, + relativeValuesExports: true, + }, + }, + }, + }); + if (!model.currentRevision) { + throw new Error("currentRevision is missing"); + } + if ( + model.currentRevision.contentType !== "SquiggleSnippet" || + !model.currentRevision.squiggleSnippet + ) { + throw new Error("Not a Squiggle model"); + } + + const revision = await tx.modelRevision.create({ + data: { + squiggleSnippet: { + create: { + code: model.currentRevision.squiggleSnippet.code, + version: input.version, + seed: model.currentRevision.squiggleSnippet.seed, + }, + }, + contentType: "SquiggleSnippet", + model: { + connect: { id: model.id }, + }, + author: { + connect: { email: self.email }, + }, + comment: `Automated upgrade from ${model.currentRevision.squiggleSnippet.version} to ${input.version}`, + relativeValuesExports: { + createMany: { + data: model.currentRevision.relativeValuesExports.map((exp) => ({ + variableName: exp.variableName, + definitionId: exp.definitionId, + })), + }, + }, + }, + include: { + model: { + select: { + id: true, + }, + }, + }, + }); + + return await tx.model.update({ + where: { + id: revision.model.id, + }, + data: { + currentRevisionId: revision.id, + updatedAt: model.updatedAt, + }, + }); + }); + + return { model }; + }); diff --git a/packages/hub/src/models/actions/createModelAction.ts b/packages/hub/src/models/actions/createModelAction.ts new file mode 100644 index 0000000000..4fb39c2b0f --- /dev/null +++ b/packages/hub/src/models/actions/createModelAction.ts @@ -0,0 +1,131 @@ +"use server"; + +import { returnValidationErrors } from "next-safe-action"; +import { z } from "zod"; + +import { generateSeed } from "@quri/squiggle-lang"; +import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; + +import { modelRoute } from "@/lib/routes"; +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableOwnerOrSelf } from "@/owners/data/auth"; +import { indexModelId } from "@/search/helpers"; +import { getSelf, getSessionOrRedirect } from "@/users/auth"; + +const defaultCode = `/* +Describe your code here +*/ + +a = normal(2, 5) +`; + +const schema = z.object({ + groupSlug: zSlug.optional(), + slug: zSlug.optional(), + isPrivate: z.boolean(), +}); + +// This action is tightly coupled with the form in NewModel.tsx. +// In particular, it uses the default code, and redirects to the newly created model. +export const createModelAction = actionClient + .schema(schema) + .action(async ({ parsedInput: input }) => { + const slug = input.slug; + if (!slug) { + returnValidationErrors(schema, { + slug: { + _errors: ["Slug is required"], + }, + }); + } + + const session = await getSessionOrRedirect(); + + const seed = generateSeed(); + const version = defaultSquiggleVersion; + const code = defaultCode; + + const model = await prisma.$transaction(async (tx) => { + const owner = await getWriteableOwnerOrSelf(input.groupSlug); + + const model = await failValidationOnConstraint( + () => + // nested create is not possible here; + // similar problem is described here: https://github.com/prisma/prisma/discussions/14937, + // seems to be caused by multiple Model -> ModelRevision relations + tx.model.create({ + data: { + slug, + ownerId: owner.id, + isPrivate: input.isPrivate, + }, + select: { id: true }, + }), + { + schema, + handlers: [ + { + constraint: ["slug", "ownerId"], + input: "slug", + error: `Model ${input.slug} already exists on this account`, + }, + ], + } + ); + + const self = await getSelf(session); + + const revision = await tx.modelRevision.create({ + data: { + squiggleSnippet: { + create: { + code, + version, + seed, + }, + }, + author: { + connect: { id: self.id }, + }, + contentType: "SquiggleSnippet", + model: { + connect: { + id: model.id, + }, + }, + }, + }); + + return await tx.model.update({ + where: { + id: model.id, + }, + data: { + currentRevisionId: revision.id, + }, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }); + }); + + await indexModelId(model.id); + + return { + url: modelRoute({ + owner: model.owner.slug, + slug: model.slug, + }), + }; + }); diff --git a/packages/hub/src/models/actions/deleteModelAction.ts b/packages/hub/src/models/actions/deleteModelAction.ts new file mode 100644 index 0000000000..88a2c219df --- /dev/null +++ b/packages/hub/src/models/actions/deleteModelAction.ts @@ -0,0 +1,28 @@ +"use server"; + +import { z } from "zod"; + +import { actionClient } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableModel } from "@/models/utils"; + +export const deleteModelAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + }) + ) + .action(async ({ parsedInput: input }) => { + const model = await getWriteableModel({ + slug: input.slug, + owner: input.owner, + }); + + await prisma.model.delete({ + where: { id: model.id }, + }); + + return { ok: true }; + }); diff --git a/packages/hub/src/models/actions/loadModelCardAction.ts b/packages/hub/src/models/actions/loadModelCardAction.ts new file mode 100644 index 0000000000..4668f099d0 --- /dev/null +++ b/packages/hub/src/models/actions/loadModelCardAction.ts @@ -0,0 +1,22 @@ +"use server"; +import { z } from "zod"; + +import { actionClient } from "@/lib/server/actionClient"; +import { zSlug } from "@/lib/zodUtils"; + +import { loadModelCard, ModelCardDTO } from "../data/cards"; + +// Data-fetching action, used in ImportTooltip. +// Don't use this for loading models; server actions are discouraged for data fetching. +export const loadModelCardAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + }) + ) + .action( + async ({ parsedInput: { owner, slug } }): Promise => { + return loadModelCard({ owner, slug }); + } + ); diff --git a/packages/hub/src/models/actions/loadModelFullAction.ts b/packages/hub/src/models/actions/loadModelFullAction.ts new file mode 100644 index 0000000000..89b066c5b4 --- /dev/null +++ b/packages/hub/src/models/actions/loadModelFullAction.ts @@ -0,0 +1,22 @@ +"use server"; +import { z } from "zod"; + +import { actionClient } from "@/lib/server/actionClient"; +import { zSlug } from "@/lib/zodUtils"; + +import { loadModelFull, ModelFullDTO } from "../data/full"; + +// Data-fetching action, used in /admin/upgrade-versions. +// Don't use this for loading models; server actions are discouraged for data fetching. +export const loadModelFullAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + }) + ) + .action( + async ({ parsedInput: { owner, slug } }): Promise => { + return loadModelFull({ owner, slug }); + } + ); diff --git a/packages/hub/src/models/actions/moveModelAction.ts b/packages/hub/src/models/actions/moveModelAction.ts new file mode 100644 index 0000000000..19829a0226 --- /dev/null +++ b/packages/hub/src/models/actions/moveModelAction.ts @@ -0,0 +1,58 @@ +"use server"; + +import { z } from "zod"; + +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableModel } from "@/models/utils"; +import { getWriteableOwnerBySlug } from "@/owners/data/auth"; + +const schema = z.object({ + oldOwner: zSlug, + // intentionally nested, matches the form shape, so that we report the error correctly + owner: z.object({ + slug: zSlug, + }), + slug: zSlug, +}); + +export const moveModelAction = actionClient + .schema(schema) + .action(async ({ parsedInput: input }) => { + const model = await getWriteableModel({ + owner: input.oldOwner, + slug: input.slug, + }); + + const newOwner = await getWriteableOwnerBySlug(input.owner.slug); + + const newModel = await failValidationOnConstraint( + () => + prisma.model.update({ + where: { id: model.id }, + data: { ownerId: newOwner.id }, + select: { + slug: true, + owner: true, + }, + }), + { + schema, + handlers: [ + { + constraint: ["slug", "ownerId"], + // `owner`, not `owner{ slug }` - the select name from the RHF point of view is just `owner`. + // (`rethrowOnConstraint` doesn't support nested keys, so we're lucky this is possible) + input: "owner", + error: `Model ${input.owner.slug} already exists on the target account`, + }, + ], + } + ); + + return { model: newModel }; + }); diff --git a/packages/hub/src/models/actions/updateModelPrivacyAction.ts b/packages/hub/src/models/actions/updateModelPrivacyAction.ts new file mode 100644 index 0000000000..b3ca61b380 --- /dev/null +++ b/packages/hub/src/models/actions/updateModelPrivacyAction.ts @@ -0,0 +1,34 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { modelRoute } from "@/lib/routes"; +import { actionClient } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableModel } from "@/models/utils"; + +export const updateModelPrivacyAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + isPrivate: z.boolean(), + }) + ) + .action(async ({ parsedInput: input }) => { + const model = await getWriteableModel({ + slug: input.slug, + owner: input.owner, + }); + + const newModel = await prisma.model.update({ + where: { id: model.id }, + data: { isPrivate: input.isPrivate }, + select: { id: true, isPrivate: true }, + }); + + revalidatePath(modelRoute({ owner: input.owner, slug: input.slug })); + return { isPrivate: newModel.isPrivate }; + }); diff --git a/packages/hub/src/models/actions/updateModelSlugAction.ts b/packages/hub/src/models/actions/updateModelSlugAction.ts new file mode 100644 index 0000000000..58da0303aa --- /dev/null +++ b/packages/hub/src/models/actions/updateModelSlugAction.ts @@ -0,0 +1,76 @@ +"use server"; + +import { z } from "zod"; + +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableModel } from "@/models/utils"; + +const schema = z.object({ + owner: zSlug, + oldSlug: zSlug, + slug: zSlug, +}); + +export const updateModelSlugAction = actionClient + .schema(schema) + .outputSchema( + z.object({ + model: z.object({ + slug: zSlug, + owner: z.object({ + slug: zSlug, + }), + }), + }) + ) + .action(async ({ parsedInput: input }) => { + const model = await getWriteableModel({ + owner: input.owner, + slug: input.oldSlug, + }); + + if (model.slug === input.slug) { + // no need to do anything + return { + model: { + slug: model.slug, + owner: { + slug: input.owner, + }, + }, + }; + } + + const newModel = await failValidationOnConstraint( + () => + prisma.model.update({ + where: { id: model.id }, + data: { slug: input.slug }, + select: { + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }), + { + schema, + handlers: [ + { + constraint: ["slug"], + input: "slug", + error: `Model ${input.slug} already exists`, + }, + ], + } + ); + + return { model: newModel }; + }); diff --git a/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts b/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts new file mode 100644 index 0000000000..328fd63f38 --- /dev/null +++ b/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts @@ -0,0 +1,158 @@ +"use server"; +import { RelativeValuesDefinition } from "@prisma/client"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { squiggleVersions } from "@quri/versioned-squiggle-components"; + +import { modelRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getSelf, getSessionOrRedirect } from "@/users/auth"; + +import { getWriteableModel } from "../utils"; + +export const updateSquiggleSnippetModelAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + relativeValuesExports: z.array( + z.object({ + variableName: z.string(), + definition: z.object({ + owner: zSlug, + slug: zSlug, + }), + }) + ), + content: z.object({ + code: z.string(), + version: z.string(), + seed: z.string(), + autorunMode: z.boolean().nullable(), + sampleCount: z.number().nullable(), + xyPointLength: z.number().nullable(), + }), + comment: z.string().optional(), + }) + ) + .action(async ({ parsedInput: input }) => { + const session = await getSessionOrRedirect(); + + const existingModel = await getWriteableModel({ + slug: input.slug, + owner: input.owner, + }); + + const version = input.content.version; + if (!(squiggleVersions as readonly string[]).includes(version)) { + throw new ActionError(`Unknown Squiggle version ${version}`); + } + + const relativeValuesExports = input.relativeValuesExports ?? []; + + const relativeValuesExportsToInsert: { + definitionId: string; + variableName: string; + }[] = []; + + if (relativeValuesExports.length) { + const selectedDefinitions = + await prisma.relativeValuesDefinition.findMany({ + where: { + OR: relativeValuesExports.map((pair) => ({ + slug: pair.definition.slug, + owner: { slug: pair.definition.owner }, + })), + }, + include: { owner: true }, + }); + + // username -> slug -> Definition + let linkedDefinitions: Map< + string, + Map + > = new Map(); + // now we need to match relativeValuesExports with definitions to get ids; I wonder if this could be simplified without sacrificing safety + for (const definition of selectedDefinitions) { + const { slug: ownerSlug } = definition.owner; + if (ownerSlug === null) { + continue; // should never happen + } + if (!linkedDefinitions.has(ownerSlug)) { + linkedDefinitions.set(ownerSlug, new Map()); + } + linkedDefinitions.get(ownerSlug)?.set(definition.slug, definition); + } + for (const pair of relativeValuesExports) { + const definition = linkedDefinitions + .get(pair.definition.owner) + ?.get(pair.definition.slug); + + if (!definition) { + throw new ActionError( + `Definition with owner ${pair.definition.owner}, slug ${pair.definition.slug} not found` + ); + } + relativeValuesExportsToInsert.push({ + variableName: pair.variableName, + definitionId: definition.id, + }); + } + } + + const self = await getSelf(session); + + const model = await prisma.$transaction(async (tx) => { + const revision = await tx.modelRevision.create({ + data: { + squiggleSnippet: { + create: { + code: input.content.code, + version: input.content.version, + seed: input.content.seed, + autorunMode: input.content.autorunMode, + sampleCount: input.content.sampleCount, + xyPointLength: input.content.xyPointLength, + }, + }, + contentType: "SquiggleSnippet", + comment: input.comment ?? "", + model: { + connect: { id: existingModel.id }, + }, + author: { + connect: { id: self.id }, + }, + relativeValuesExports: { + createMany: { + data: relativeValuesExportsToInsert, + }, + }, + }, + include: { + model: { + select: { + id: true, + }, + }, + }, + }); + const updatedModel = await tx.model.update({ + where: { + id: revision.modelId, + }, + data: { + currentRevisionId: revision.id, + }, + }); + + return updatedModel; + }); + + revalidatePath(modelRoute({ owner: input.owner, slug: input.slug })); + + return { model }; + }); diff --git a/packages/hub/src/models/clientUtils.ts b/packages/hub/src/models/clientUtils.ts new file mode 100644 index 0000000000..456ed42baa --- /dev/null +++ b/packages/hub/src/models/clientUtils.ts @@ -0,0 +1,29 @@ +// TODO - should use versioned-components +import { ASTNode, parse } from "@quri/squiggle-lang"; + +function astToVariableNames(ast: ASTNode): string[] { + const exportedVariableNames: string[] = []; + + if (ast.kind === "Program") { + ast.statements.forEach((statement) => { + if ( + (statement.kind === "LetStatement" || + statement.kind === "DefunStatement") && + statement.exported + ) { + exportedVariableNames.push(statement.variable.value); + } + }); + } + + return exportedVariableNames; +} + +export function getExportedVariableNames(code: string): string[] { + const ast = parse(code); + if (ast.ok) { + return astToVariableNames(ast.value); + } else { + return []; + } +} diff --git a/packages/hub/src/models/components/ModelCard.tsx b/packages/hub/src/models/components/ModelCard.tsx index 0cff1193e8..97d1fa59f8 100644 --- a/packages/hub/src/models/components/ModelCard.tsx +++ b/packages/hub/src/models/components/ModelCard.tsx @@ -1,6 +1,4 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { CodeSyntaxHighlighter, NumberShower } from "@quri/squiggle-components"; import { XIcon } from "@quri/ui"; @@ -13,61 +11,16 @@ import { UpdatedStatus, } from "@/components/EntityCard"; import { Link } from "@/components/ui/Link"; +import { modelRoute, ownerRoute } from "@/lib/routes"; +import { ModelCardDTO } from "@/models/data/cards"; import { totalImportLength, VariableRevision, VariablesDropdown, -} from "@/lib/VariablesDropdown"; -import { modelRoute, ownerRoute } from "@/routes"; - -import { ModelCard$key } from "@/__generated__/ModelCard.graphql"; - -const Fragment = graphql` - fragment ModelCard on Model { - id - slug - updatedAtTimestamp - owner { - __typename - slug - } - isPrivate - variables { - variableName - currentRevision { - variableType - title - } - } - currentRevision { - content { - __typename - ... on SquiggleSnippet { - id - code - version - seed - autorunMode - sampleCount - xyPointLength - } - } - relativeValuesExports { - variableName - definition { - slug - } - } - buildStatus - lastBuild { - runSeconds - } - } - } -`; +} from "@/variables/components/VariablesDropdown"; type Props = { - modelRef: ModelCard$key; + model: ModelCardDTO; showOwner?: boolean; }; @@ -104,16 +57,9 @@ const RunTime: FC<{ seconds: number }> = ({ seconds }) => (
); -export const ModelCard: FC = ({ modelRef, showOwner = true }) => { - const model = useFragment(Fragment, modelRef); - const { - owner, - slug, - updatedAtTimestamp, - isPrivate, - variables, - currentRevision, - } = model; +export const ModelCard: FC = ({ model, showOwner = true }) => { + const { owner, slug, updatedAt, isPrivate, variables, currentRevision } = + model; const variableRevisions: VariableRevision[] = variables.map((v) => ({ variableName: v.variableName, @@ -133,9 +79,11 @@ export const ModelCard: FC = ({ modelRef, showOwner = true }) => { variableRevisions, relativeValuesExports ); - const { buildStatus, lastBuild, content } = currentRevision; - const body = - content.__typename === "SquiggleSnippet" ? content.code : undefined; + const { + // buildStatus, lastBuild, + squiggleSnippet, + } = currentRevision; + const body = squiggleSnippet?.code; const menuItems = ( = ({ modelRef, showOwner = true }) => { ), isPrivate && , - updatedAtTimestamp && ( - + updatedAt && ( + ), ]} /> diff --git a/packages/hub/src/models/components/ModelList.tsx b/packages/hub/src/models/components/ModelList.tsx index 88a98bac9a..891734166f 100644 --- a/packages/hub/src/models/components/ModelList.tsx +++ b/packages/hub/src/models/components/ModelList.tsx @@ -1,52 +1,29 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { LoadMore } from "@/components/LoadMore"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; +import { ModelCardDTO } from "@/models/data/cards"; import { ModelCard } from "./ModelCard"; -import { ModelList$key } from "@/__generated__/ModelList.graphql"; - -const Fragment = graphql` - fragment ModelList on ModelConnection { - edges { - node { - id - ...ModelCard - } - } - pageInfo { - hasNextPage - } - } -`; - type Props = { - connectionRef: ModelList$key; - loadNext(count: number): unknown; + page: Paginated; showOwner?: boolean; }; -export const ModelList: FC = ({ - connectionRef, - loadNext, - showOwner, -}) => { - const connection = useFragment(Fragment, connectionRef); +export const ModelList: FC = ({ page, showOwner }) => { + const { items: models, loadNext } = usePaginator(page); return (
- {connection.edges.map((edge) => ( - + {models.map((model) => ( + ))}
- {connection.pageInfo.hasNextPage && } + {loadNext && }
); }; diff --git a/packages/hub/src/models/data/authHelpers.ts b/packages/hub/src/models/data/authHelpers.ts new file mode 100644 index 0000000000..028f33bfe8 --- /dev/null +++ b/packages/hub/src/models/data/authHelpers.ts @@ -0,0 +1,30 @@ +import { Prisma } from "@prisma/client"; + +import { auth } from "@/lib/server/auth"; + +export async function modelWhereHasAccess(): Promise { + const session = await auth(); + + const orParts: Prisma.ModelWhereInput[] = [{ isPrivate: false }]; + if (session) { + orParts.push({ + owner: { + OR: [ + { + user: { email: session.user.email }, + }, + { + group: { + memberships: { + some: { + user: { email: session.user.email }, + }, + }, + }, + }, + ], + }, + }); + } + return orParts; +} diff --git a/packages/hub/src/models/data/byVersion.ts b/packages/hub/src/models/data/byVersion.ts new file mode 100644 index 0000000000..d365cda0c8 --- /dev/null +++ b/packages/hub/src/models/data/byVersion.ts @@ -0,0 +1,51 @@ +import { prisma } from "@/lib/server/prisma"; +import { checkRootUser } from "@/users/auth"; + +// Admin-only, for listing models in /admin UI +export async function loadModelsByVersion() { + await checkRootUser(); + + const models = await prisma.model.findMany({ + include: { + currentRevision: { + where: { + contentType: "SquiggleSnippet", + }, + include: { + squiggleSnippet: true, + }, + }, + owner: true, + }, + }); + + const groupedModels: Record = {}; + const privateStats: Record = {}; + const versions = new Set(); + + for (const model of models) { + const version = model.currentRevision?.squiggleSnippet?.version; + if (!version) continue; + + versions.add(version); + + if (model.isPrivate) { + privateStats[version] ??= 0; + privateStats[version]++; + } else { + // don't expose private models, they won't be available for loading anyway + groupedModels[version] ??= []; + groupedModels[version].push(model); + } + } + return [...versions.values()].map((version) => ({ + version, + count: (groupedModels[version] ?? []).length, + privateCount: privateStats[version] ?? 0, + models: groupedModels[version] ?? [], + })); +} + +export type ModelByVersion = Awaited< + ReturnType +>[number]; diff --git a/packages/hub/src/models/data/cards.ts b/packages/hub/src/models/data/cards.ts new file mode 100644 index 0000000000..ddcca98a8a --- /dev/null +++ b/packages/hub/src/models/data/cards.ts @@ -0,0 +1,152 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; +import { selectTypedOwner, toTypedOwnerDTO } from "@/owners/data/typedOwner"; + +import { modelWhereHasAccess } from "./authHelpers"; + +// FIXME - explicit ModelCardDTO +function toDTO(dbModel: DbModelCard) { + function check(model: DbModelCard): asserts model is Omit< + DbModelCard, + "currentRevision" + > & { + currentRevision: NonNullable; + } { + if (!model.currentRevision) { + throw new Error("Model has no current revision"); + } + } + check(dbModel); + + return { + // FIXME - process each field separately + ...dbModel, + owner: toTypedOwnerDTO(dbModel.owner), + }; +} + +const select = { + id: true, + slug: true, + updatedAt: true, + owner: { + select: selectTypedOwner, + }, + isPrivate: true, + variables: { + select: { + variableName: true, + currentRevision: { + select: { + variableType: true, + title: true, + }, + }, + }, + }, + currentRevision: { + select: { + id: true, + contentType: true, + squiggleSnippet: { + select: { + id: true, + code: true, + version: true, + }, + }, + relativeValuesExports: { + select: { + variableName: true, + definition: { + select: { + slug: true, + }, + }, + }, + }, + builds: { + select: { + runSeconds: true, + errors: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 1, + }, + }, + }, +} satisfies Prisma.ModelSelect; + +type DbModelCard = NonNullable< + Awaited>> +>; + +export type ModelCardDTO = ReturnType; + +export async function loadModelCards( + params: { + ownerSlug?: string; + cursor?: string; + limit?: number; + } = {} +): Promise> { + const limit = params.limit ?? 20; + + const dbModels = await prisma.model.findMany({ + select, + orderBy: { updatedAt: "desc" }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: { + ...(params.ownerSlug + ? { + owner: { + slug: params.ownerSlug, + }, + } + : {}), + OR: await modelWhereHasAccess(), + }, + take: limit + 1, + }); + + const models = dbModels.map(toDTO); + + const nextCursor = models[models.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadModelCards({ ...params, cursor: nextCursor, limit }); + } + + return { + items: models.slice(0, limit), + loadMore: models.length > limit ? loadMore : undefined, + }; +} + +export async function loadModelCard({ + owner, + slug, +}: { + owner: string; + slug: string; +}): Promise { + const dbModel = await prisma.model.findFirst({ + select: select, + where: { + slug, + owner: { slug: owner }, + OR: await modelWhereHasAccess(), + }, + }); + + if (!dbModel) { + return null; + } + + return toDTO(dbModel); +} diff --git a/packages/hub/src/models/data/full.ts b/packages/hub/src/models/data/full.ts new file mode 100644 index 0000000000..ec0503121a --- /dev/null +++ b/packages/hub/src/models/data/full.ts @@ -0,0 +1,115 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { controlsOwnerId } from "@/owners/data/auth"; + +import { modelWhereHasAccess } from "./authHelpers"; +import { + ModelRevisionFullDTO, + modelRevisionFullToDTO, + selectModelRevisionFull, +} from "./fullRevision"; + +const select = { + id: true, + slug: true, + owner: { + select: { + id: true, + slug: true, + }, + }, + currentRevision: { + select: selectModelRevisionFull, + }, +} satisfies Prisma.ModelSelect; + +type Row = NonNullable< + Awaited>> +>; + +export type ModelFullDTO = { + id: string; + slug: string; + owner: { + id: string; + slug: string; + }; + currentRevision: ModelRevisionFullDTO; + isEditable: boolean; + lastBuildSeconds: number | null; +}; + +async function toDTO(row: Row): Promise { + if (!row.currentRevision) { + throw new Error("No current revision"); + } + + if (!row.currentRevision.squiggleSnippet) { + throw new Error("No squiggle snippet"); + } + + let lastBuildSeconds: number | null = null; + { + const lastRevisionWithBuild = await prisma.modelRevision.findFirst({ + select: { + builds: { + select: { + runSeconds: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + where: { + modelId: row.id, + builds: { + some: { + id: { + not: undefined, + }, + }, + }, + }, + }); + if (lastRevisionWithBuild) { + lastBuildSeconds = lastRevisionWithBuild.builds[0]?.runSeconds; + } + } + + return { + id: row.id, + slug: row.slug, + owner: { + id: row.owner.id, + slug: row.owner.slug, + }, + currentRevision: await modelRevisionFullToDTO(row.currentRevision), + isEditable: await controlsOwnerId(row.owner.id), + lastBuildSeconds, + }; +} + +export async function loadModelFull({ + owner, + slug, +}: { + owner: string; + slug: string; +}): Promise { + const row = await prisma.model.findFirst({ + select, + where: { + slug: slug, + owner: { slug: owner }, + OR: await modelWhereHasAccess(), + }, + }); + + if (!row) { + return null; + } + + return toDTO(row); +} diff --git a/packages/hub/src/models/data/fullRevision.ts b/packages/hub/src/models/data/fullRevision.ts new file mode 100644 index 0000000000..10db8a86f0 --- /dev/null +++ b/packages/hub/src/models/data/fullRevision.ts @@ -0,0 +1,109 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; + +import { modelWhereHasAccess } from "./authHelpers"; + +export const selectModelRevisionFull = { + id: true, + createdAt: true, + comment: true, + contentType: true, + squiggleSnippet: { + select: { + id: true, + code: true, + version: true, + seed: true, + autorunMode: true, + sampleCount: true, + xyPointLength: true, + }, + }, + relativeValuesExports: { + select: { + variableName: true, + definition: { + select: { + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }, + }, + }, +} satisfies Prisma.ModelRevisionSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.modelRevision.findFirst<{ + select: typeof selectModelRevisionFull; + }> + > + > +>; + +export type ModelRevisionFullDTO = { + id: string; + createdAt: Date; + comment: string; + contentType: "SquiggleSnippet"; + squiggleSnippet: { + id: string; + code: string; + version: string; + seed: string; + autorunMode: boolean | null; + sampleCount: number | null; + xyPointLength: number | null; + }; + relativeValuesExports: { + variableName: string; + definition: { + slug: string; + owner: { slug: string }; + }; + }[]; +}; + +export async function modelRevisionFullToDTO( + row: Row +): Promise { + // FIXME - validate + if (!row.squiggleSnippet) { + throw new Error("Unknown model type"); + } + return row as typeof row & { squiggleSnippet: typeof row.squiggleSnippet }; +} + +export async function loadModelRevisionFull({ + owner, + slug, + revisionId, +}: { + owner: string; + slug: string; + revisionId: string; +}): Promise { + const dbRevision = await prisma.modelRevision.findFirst({ + where: { + id: revisionId, + model: { + slug, + owner: { slug: owner }, + OR: await modelWhereHasAccess(), + }, + }, + select: selectModelRevisionFull, + }); + + if (!dbRevision) { + return null; + } + + return modelRevisionFullToDTO(dbRevision); +} diff --git a/packages/hub/src/models/data/helpers.ts b/packages/hub/src/models/data/helpers.ts new file mode 100644 index 0000000000..2f12804af9 --- /dev/null +++ b/packages/hub/src/models/data/helpers.ts @@ -0,0 +1,7 @@ +import { controlsOwnerId } from "@/owners/data/auth"; + +import { ModelCardDTO } from "./cards"; + +export async function isModelEditable(model: ModelCardDTO): Promise { + return controlsOwnerId(model.owner.id); +} diff --git a/packages/hub/src/models/data/revisions.ts b/packages/hub/src/models/data/revisions.ts new file mode 100644 index 0000000000..c52c0f2da3 --- /dev/null +++ b/packages/hub/src/models/data/revisions.ts @@ -0,0 +1,136 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; + +import { modelWhereHasAccess } from "./authHelpers"; + +export const selectModelRevision = { + id: true, + createdAt: true, + author: { + select: { asOwner: { select: { slug: true } } }, + }, + comment: true, + variableRevisions: { + select: { + id: true, + }, + }, + // used for `buildStatus` and `lastBuild` + builds: { + select: { + errors: true, + runSeconds: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 1, + }, + model: { + select: { + currentRevisionId: true, + }, + }, +} satisfies Prisma.ModelRevisionSelect; + +type BuildStatus = "Success" | "Failure" | "Pending" | "Skipped"; + +type DbModelRevision = NonNullable< + Awaited< + ReturnType< + typeof prisma.modelRevision.findFirst<{ + select: typeof selectModelRevision; + }> + > + > +>; + +type DbModelRevisionBuild = DbModelRevision["builds"][number]; + +type ModelRevisionBuildDTO = { + runSeconds: number; + errors: string[]; +}; + +export type ModelRevisionDTO = { + id: string; + createdAt: Date; + author?: { username: string }; + comment?: string; + variableRevisions: { id: string }[]; + buildStatus: BuildStatus; + lastBuild?: ModelRevisionBuildDTO; +}; + +function buildToDTO(build: DbModelRevisionBuild): ModelRevisionBuildDTO { + return { + runSeconds: build.runSeconds, + errors: build.errors, + }; +} + +export function modelRevisionToDTO( + dbRevision: DbModelRevision +): ModelRevisionDTO { + const lastBuild = dbRevision.builds[0]; + let buildStatus: BuildStatus = "Pending"; + if (lastBuild) { + const errors = lastBuild.errors.filter((e) => e !== ""); + buildStatus = errors.length === 0 ? "Success" : "Failure"; + } else if (dbRevision.model.currentRevisionId !== dbRevision.id) { + buildStatus = "Skipped"; + } + + return { + id: dbRevision.id, + createdAt: dbRevision.createdAt, + author: dbRevision.author?.asOwner + ? { + username: dbRevision.author.asOwner?.slug, + } + : undefined, + comment: dbRevision.comment, + variableRevisions: dbRevision.variableRevisions, + buildStatus, + lastBuild: lastBuild ? buildToDTO(lastBuild) : undefined, + }; +} + +export async function loadModelRevisions(params: { + owner: string; + slug: string; + cursor?: string; + limit?: number; +}): Promise> { + const limit = params.limit ?? 20; + + const dbRevisions = await prisma.modelRevision.findMany({ + where: { + model: { + slug: params.slug, + owner: { slug: params.owner }, + OR: await modelWhereHasAccess(), + }, + }, + cursor: params.cursor ? { id: params.cursor } : undefined, + orderBy: { createdAt: "desc" }, + select: selectModelRevision, + take: limit + 1, + }); + + const nextCursor = dbRevisions[dbRevisions.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadModelRevisions({ ...params, cursor: nextCursor, limit }); + } + + const revisions = dbRevisions.map(modelRevisionToDTO); + + return { + items: revisions.slice(0, limit), + loadMore: revisions.length > limit ? loadMore : undefined, + }; +} diff --git a/packages/hub/src/models/utils.ts b/packages/hub/src/models/utils.ts new file mode 100644 index 0000000000..716941e26a --- /dev/null +++ b/packages/hub/src/models/utils.ts @@ -0,0 +1,70 @@ +import { Model, Prisma } from "@prisma/client"; + +import { ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { getSessionOrRedirect } from "@/users/auth"; + +import { modelWhereHasAccess } from "./data/authHelpers"; + +export async function getWriteableModel({ + owner, + slug, + include, +}: { + owner: string; + slug: string; + include?: Prisma.ModelInclude; +}): Promise { + const session = await getSessionOrRedirect(); + + // Note: `findUnique` would be safer, but then we won't be able to use nested queries + const model = await prisma.model.findFirst({ + where: { + slug, + owner: { + slug: owner, + OR: [ + { + user: { email: session.user.email }, + }, + { + group: { + memberships: { + some: { + user: { email: session.user.email }, + }, + }, + }, + }, + ], + }, + }, + include, + }); + + if (!model) { + // we're going to fail, but how? + + // does the model exist? + const modelExists = !!(await prisma.model.findFirst({ + select: { + id: true, + }, + where: { + slug, + owner: { + slug: owner, + }, + OR: await modelWhereHasAccess(), + }, + })); + + if (modelExists) { + throw new ActionError("Can't edit model"); + } else { + throw new ActionError("Can't find model"); + } + } + + return model; +} diff --git a/packages/hub/src/graphql/helpers/ownerHelpers.ts b/packages/hub/src/owners/data/auth.ts similarity index 55% rename from packages/hub/src/graphql/helpers/ownerHelpers.ts rename to packages/hub/src/owners/data/auth.ts index 74eef0510b..6cc7b3531f 100644 --- a/packages/hub/src/graphql/helpers/ownerHelpers.ts +++ b/packages/hub/src/owners/data/auth.ts @@ -1,8 +1,41 @@ -import { Session } from "next-auth"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { getSessionOrRedirect } from "@/users/auth"; -import { prisma } from "@/prisma"; +export async function controlsOwnerId(ownerId: string): Promise { + const session = await auth(); + if (!session?.user.email) { + return false; + } + + return Boolean( + await prisma.owner.count({ + where: { + id: ownerId, + OR: [ + { + user: { email: session.user.email }, + }, + { + group: { + memberships: { + some: { + user: { + email: session.user.email, + }, + }, + }, + }, + }, + ], + }, + }) + ); +} + +export async function getWriteableOwnerBySlug(slug: string) { + const session = await getSessionOrRedirect(); -export async function getWriteableOwnerBySlug(session: Session, slug: string) { const owner = await prisma.owner.findFirst({ where: { slug, @@ -33,11 +66,11 @@ export async function getWriteableOwnerBySlug(session: Session, slug: string) { return owner; } -// deprecated, need to migrate to getWriteableOwnerBySlug everywhere -export async function getWriteableOwner( - session: Session, +export async function getWriteableOwnerOrSelf( groupSlug?: string | null | undefined ) { + const session = await getSessionOrRedirect(); + const owner = await prisma.owner.findFirst({ where: { ...(groupSlug diff --git a/packages/hub/src/owners/data/findOwners.ts b/packages/hub/src/owners/data/findOwners.ts new file mode 100644 index 0000000000..785b14ccc6 --- /dev/null +++ b/packages/hub/src/owners/data/findOwners.ts @@ -0,0 +1,129 @@ +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; + +type OwnerForSelect = { + __typename: "User" | "Group"; + id: string; + slug: string; +}; + +// See also: app/api/find-owners/route.ts +export async function findOwnersForSelect(params: { + search: string; + // "my" means "me and my groups" + mode: "all" | "all-users" | "all-groups" | "my" | "my-groups"; +}): Promise { + const result: OwnerForSelect[] = []; + const session = await auth(); + + const selectUsers = params.mode === "all-users" || params.mode === "all"; + + const selectGroups = + params.mode === "all" || + params.mode === "all-groups" || + params.mode === "my-groups" || + params.mode === "my"; + + const myOnly = params.mode === "my" || params.mode === "my-groups"; + + if (myOnly && !session?.user.email) { + return []; + } + + if ( + (selectUsers || params.mode === "my") && + session?.user.id && + session?.user?.username && + (!params.search || session.user.username.match(params.search)) + ) { + result.push({ + __typename: "User" as const, + id: session.user.id, + slug: session.user.username ?? "", + }); + } + + if (selectUsers) { + const rows = await prisma.user.findMany({ + where: { + ownerId: { not: null }, + ...(params.search && { + asOwner: { + slug: { + contains: params.search, + mode: "insensitive", + }, + }, + }), + }, + select: { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + }, + take: 20, + }); + + for (const row of rows) { + if (!row.asOwner) { + continue; + } + if (row.id === session?.user.id) { + // already added above + continue; + } + + result.push({ + __typename: "User" as const, + id: row.id, + slug: row.asOwner.slug, + }); + } + } + + if (selectGroups) { + const rows = await prisma.group.findMany({ + where: { + ...(params.search && { + asOwner: { + slug: { + contains: params.search, + mode: "insensitive", + }, + }, + }), + ...(myOnly + ? { + memberships: { + some: { + user: { email: session!.user.email }, + }, + }, + } + : {}), + }, + select: { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + }, + take: 20, + }); + + result.push( + ...rows.map((row) => ({ + __typename: "Group" as const, + id: row.id, + slug: row.asOwner?.slug ?? "", + })) + ); + } + + return result; +} diff --git a/packages/hub/src/owners/data/typedOwner.ts b/packages/hub/src/owners/data/typedOwner.ts new file mode 100644 index 0000000000..75332616e7 --- /dev/null +++ b/packages/hub/src/owners/data/typedOwner.ts @@ -0,0 +1,38 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; + +export type TypedOwner = { + __typename: "User" | "Group"; + id: string; + slug: string; +}; + +export const selectTypedOwner = { + id: true, + slug: true, + user: { + select: { id: true }, + }, + group: { + select: { id: true }, + }, +} satisfies Prisma.OwnerSelect; + +type DbTypedOwner = NonNullable< + Awaited< + ReturnType< + typeof prisma.owner.findFirst<{ select: typeof selectTypedOwner }> + > + > +>; + +// compatible with old GraphQL format +export function toTypedOwnerDTO(dbOwner: DbTypedOwner): TypedOwner { + const __typename = dbOwner.user ? "User" : "Group"; + return { + __typename, + id: dbOwner.id, + slug: dbOwner.slug, + }; +} diff --git a/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts b/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts new file mode 100644 index 0000000000..0c297b6471 --- /dev/null +++ b/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts @@ -0,0 +1,105 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { modelForRelativeValuesExportRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { cartesianProduct } from "@/relative-values/lib/utils"; +import { relativeValuesItemsSchema } from "@/relative-values/types"; +import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; + +import { getRelativeValuesExportForWriteableModel } from "../utils"; + +export const buildRelativeValuesCacheAction = actionClient + .schema( + z.object({ + exportId: z.string(), + }) + ) + .action(async ({ parsedInput: { exportId } }): Promise => { + const relativeValuesExport = await getRelativeValuesExportForWriteableModel( + { + exportId, + } + ); + + const { modelRevision } = relativeValuesExport; + + if (modelRevision.contentType !== "SquiggleSnippet") { + throw new ActionError("Unsupported model revision content type"); + } + + const squiggleSnippet = modelRevision.squiggleSnippet; + if (!squiggleSnippet) { + throw new ActionError("Model content not found"); + } + + const evaluatorResult = await ModelEvaluator.create( + squiggleSnippet.code, + relativeValuesExport.variableName + ); + if (!evaluatorResult.ok) { + throw new ActionError( + `Failed to create evaluator: ${evaluatorResult.value.toString()}` + ); + } + const evaluator = evaluatorResult.value; + + const definitionRevision = relativeValuesExport.definition.currentRevision; + if (!definitionRevision) { + throw new ActionError("Definition revision not found"); + } + + const items = relativeValuesItemsSchema.parse(definitionRevision.items); + const itemIds = items.map((item) => item.id); + + const existingCacheItems = await prisma.relativeValuesPairCache.findMany({ + where: { + exportId, + }, + select: { + firstItem: true, + secondItem: true, + }, + }); + + const seen: Record> = {}; + for (const row of existingCacheItems) { + seen[row.firstItem] ??= {}; + seen[row.firstItem][row.secondItem] = true; + } + + for (const [firstItem, secondItem] of cartesianProduct(itemIds, itemIds)) { + if (seen[firstItem]?.[secondItem]) { + continue; // already cached + } + const result = evaluator.compareWithoutCache(firstItem, secondItem); + await prisma.relativeValuesPairCache.create({ + data: { + exportId, + firstItem, + secondItem, + ...(result.ok + ? { result: result.value } + : { error: result.value.toString() }), + }, + }); + } + + // check that the export still exists - is this useful? + await prisma.relativeValuesExport.findUniqueOrThrow({ + where: { id: exportId }, + select: { + id: true, + }, + }); + + revalidatePath( + modelForRelativeValuesExportRoute({ + owner: relativeValuesExport.modelRevision.model.owner.slug, + slug: relativeValuesExport.modelRevision.model.slug, + variableName: relativeValuesExport.variableName, + }) + ); + }); diff --git a/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts b/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts new file mode 100644 index 0000000000..f54c05397e --- /dev/null +++ b/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts @@ -0,0 +1,52 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { modelForRelativeValuesExportRoute } from "@/lib/routes"; +import { actionClient } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { getRelativeValuesExportForWriteableModel } from "@/relative-values/utils"; + +export const clearRelativeValuesCacheAction = actionClient + .schema( + z.object({ + exportId: z.string(), + }) + ) + .action(async ({ parsedInput: { exportId } }): Promise => { + await getRelativeValuesExportForWriteableModel({ + exportId, + }); + + await prisma.relativeValuesPairCache.deleteMany({ + where: { exportId }, + }); + + const relativeValuesExport = + await prisma.relativeValuesExport.findUniqueOrThrow({ + where: { id: exportId }, + select: { + modelRevision: { + select: { + model: { + select: { + owner: { + select: { slug: true }, + }, + slug: true, + }, + }, + }, + }, + variableName: true, + }, + }); + + revalidatePath( + modelForRelativeValuesExportRoute({ + owner: relativeValuesExport.modelRevision.model.owner.slug, + slug: relativeValuesExport.modelRevision.model.slug, + variableName: relativeValuesExport.variableName, + }) + ); + }); diff --git a/packages/hub/src/relative-values/actions/common.ts b/packages/hub/src/relative-values/actions/common.ts new file mode 100644 index 0000000000..3f900dd0d9 --- /dev/null +++ b/packages/hub/src/relative-values/actions/common.ts @@ -0,0 +1,64 @@ +import { z } from "zod"; + +import { zColor, zSlug } from "@/lib/zodUtils"; + +// Appropriate both for create and update actions. +export const inputSchema = z.object({ + owner: zSlug.optional(), + slug: zSlug, + title: z.string(), + items: z.array( + z.object({ + id: zSlug, + name: z.string(), + description: z.string().optional(), + clusterId: z.string().optional(), + }) + ), + clusters: z.array( + z.object({ + id: zSlug, + color: zColor, + recommendedUnit: zSlug.optional(), + }) + ), + recommendedUnit: zSlug.optional(), +}); + +export type Input = z.infer; + +export async function validateRelativeValuesDefinition({ + items, + clusters, + recommendedUnit, +}: { + items: Input["items"]; + clusters: Input["clusters"]; + recommendedUnit: Input["recommendedUnit"]; +}) { + if (!items.length) { + throw new Error("RelativeValuesDefinition must include at least one item"); + } + + const itemIds = new Set(); + for (const item of items) { + if (itemIds.has(item.id)) { + throw new Error(`Duplicate item id ${item.id}`); + } + if (!item.id.match(/^\w[\w\-]*$/)) { + throw new Error(`Invalid item id ${item.id}`); + } + itemIds.add(item.id); + } + + const checkId = (id: string | null | undefined) => { + if (id !== null && id !== undefined && !itemIds.has(id)) { + throw new Error(`id ${id} not found in items`); + } + }; + + for (const cluster of clusters) { + checkId(cluster.recommendedUnit); + } + checkId(recommendedUnit); +} diff --git a/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts new file mode 100644 index 0000000000..148e59a258 --- /dev/null +++ b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts @@ -0,0 +1,101 @@ +"use server"; + +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { getWriteableOwnerOrSelf } from "@/owners/data/auth"; +import { indexDefinitionId } from "@/search/helpers"; + +import { inputSchema, validateRelativeValuesDefinition } from "./common"; + +export const createRelativeValuesDefinitionAction = actionClient + .schema(inputSchema) + .action( + async ({ + parsedInput: input, + }): Promise<{ + owner: string; + slug: string; + }> => { + const owner = await getWriteableOwnerOrSelf(input.owner); + + validateRelativeValuesDefinition({ + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }); + + const definition = await prisma.$transaction(async (tx) => { + const definition = await failValidationOnConstraint( + () => + tx.relativeValuesDefinition.create({ + data: { + ownerId: owner.id, + slug: input.slug, + revisions: { + create: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }, + }, + }, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }), + { + schema: inputSchema, + handlers: [ + { + constraint: ["slug", "ownerId"], + input: "slug", + error: `The definition ${input.slug} already exists on this account`, + }, + ], + } + ); + + const revision = await tx.relativeValuesDefinitionRevision.create({ + data: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + definition: { + connect: { + id: definition.id, + }, + }, + }, + }); + + await tx.relativeValuesDefinition.update({ + where: { + id: definition.id, + }, + data: { + currentRevisionId: revision.id, + }, + }); + + return definition; + }); + + await indexDefinitionId(definition.id); + + return { + owner: definition.owner.slug, + slug: definition.slug, + }; + } + ); diff --git a/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx new file mode 100644 index 0000000000..917d721c5f --- /dev/null +++ b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx @@ -0,0 +1,28 @@ +"use server"; +import { z } from "zod"; + +import { actionClient } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableOwnerBySlug } from "@/owners/data/auth"; + +export const deleteRelativeValuesDefinitionAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + }) + ) + .action(async ({ parsedInput: input }): Promise<"ok"> => { + const owner = await getWriteableOwnerBySlug(input.owner); + + await prisma.relativeValuesDefinition.delete({ + where: { + slug_ownerId: { + slug: input.slug, + ownerId: owner.id, + }, + }, + }); + return "ok"; + }); diff --git a/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts new file mode 100644 index 0000000000..5bb69bb5d0 --- /dev/null +++ b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts @@ -0,0 +1,69 @@ +"use server"; +import { actionClient } from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; +import { getWriteableOwnerOrSelf } from "@/owners/data/auth"; + +import { inputSchema, validateRelativeValuesDefinition } from "./common"; + +export const updateRelativeValuesDefinitionAction = actionClient + .schema(inputSchema) + .action( + async ({ + parsedInput: input, + }): Promise<{ owner: string; slug: string }> => { + const owner = await getWriteableOwnerOrSelf(input.owner); + + validateRelativeValuesDefinition({ + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }); + + const definition = await prisma.$transaction(async (tx) => { + const revision = await tx.relativeValuesDefinitionRevision.create({ + data: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + definition: { + connect: { + slug_ownerId: { + slug: input.slug, + ownerId: owner.id, + }, + }, + }, + }, + include: { + definition: { + select: { + id: true, + }, + }, + }, + }); + + const definition = await tx.relativeValuesDefinition.update({ + where: { + id: revision.definition.id, + }, + data: { + currentRevisionId: revision.id, + }, + select: { + owner: { + select: { + slug: true, + }, + }, + slug: true, + }, + }); + + return definition; + }); + + return { owner: definition.owner.slug, slug: definition.slug }; + } + ); diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx index 39fa14a795..a76ccd6e83 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx @@ -1,33 +1,18 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; -import { relativeValuesRoute } from "@/routes"; - -import { RelativeValuesDefinitionCard$key } from "@/__generated__/RelativeValuesDefinitionCard.graphql"; - -const Fragment = graphql` - fragment RelativeValuesDefinitionCard on RelativeValuesDefinition { - slug - updatedAtTimestamp - owner { - slug - } - } -`; +import { relativeValuesRoute } from "@/lib/routes"; +import { RelativeValuesDefinitionCardDTO } from "@/relative-values/data/cards"; type Props = { - definitionRef: RelativeValuesDefinitionCard$key; + definition: RelativeValuesDefinitionCardDTO; showOwner?: boolean; }; export const RelativeValuesDefinitionCard: FC = ({ - definitionRef, + definition, showOwner = true, }) => { - const definition = useFragment(Fragment, definitionRef); - return ( = ({ slug={definition.slug} menuItems={ <> - + } /> diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx index eb3b38aa1f..1fe4622bb8 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx @@ -1,11 +1,14 @@ -"use client"; - -import { FC } from "react"; -import { FormProvider, useForm } from "react-hook-form"; +import { + InferSafeActionFnInput, + InferSafeActionFnResult, +} from "next-safe-action"; +import { HookSafeActionFn } from "next-safe-action/hooks"; +import { FormProvider } from "react-hook-form"; import { Button, StyledTab, TextFormField } from "@quri/ui"; import { SlugFormField } from "@/components/ui/SlugFormField"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; import { FormShape } from "./FormShape"; import { FormSectionHeader, HTMLForm } from "./HTMLForm"; @@ -17,15 +20,31 @@ type Props = { save: (data: FormShape) => Promise; }; -export const RelativeValuesDefinitionForm: FC = ({ +export function RelativeValuesDefinitionForm< + Action extends HookSafeActionFn, +>({ defaultValues, withoutSlug, - save, -}) => { - const form = useForm({ defaultValues }); - - const onSubmit = form.handleSubmit(async (data) => { - await save(data); + action, + formDataToInput, + onSuccess, +}: { + defaultValues?: FormShape; + withoutSlug?: boolean; + action: Action; + formDataToInput: ( + data: FormShape + ) => InferSafeActionFnInput["clientInput"]; + onSuccess?: ( + data: NonNullable["data"]> + ) => void; +}) { + const { form, onSubmit, inFlight } = useSafeActionForm({ + mode: "onChange", + defaultValues, + action, + formDataToInput: formDataToInput, + onSuccess, }); return ( @@ -65,11 +84,11 @@ export const RelativeValuesDefinitionForm: FC = ({
-
); -}; +} diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx index 6199482c6e..fa80b73388 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx @@ -1,53 +1,41 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { LoadMore } from "@/components/LoadMore"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; +import { RelativeValuesDefinitionCardDTO } from "@/relative-values/data/cards"; import { RelativeValuesDefinitionCard } from "./RelativeValuesDefinitionCard"; -import { RelativeValuesDefinitionList$key } from "@/__generated__/RelativeValuesDefinitionList.graphql"; - -const Fragment = graphql` - fragment RelativeValuesDefinitionList on RelativeValuesDefinitionConnection { - edges { - node { - id - ...RelativeValuesDefinitionCard - } - } - pageInfo { - hasNextPage - } - } -`; - type Props = { - connectionRef: RelativeValuesDefinitionList$key; - loadNext(count: number): unknown; + page: Paginated; showOwner?: boolean; }; export const RelativeValuesDefinitionList: FC = ({ - connectionRef, - loadNext, + page: initialPage, showOwner, }) => { - const connection = useFragment(Fragment, connectionRef); + const page = usePaginator(initialPage); + + if (!page.items.length) { + return
No definitions to show.
; + } return (
- {connection.edges.map((edge) => ( + {page.items.map((definition) => ( ))}
- {connection.pageInfo.hasNextPage && } + {page.loadNext && }
); }; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx index 78862f901f..0263cc14ab 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx @@ -1,45 +1,19 @@ import { FC, Fragment } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { StyledTab, StyledTextArea } from "@quri/ui"; import { H2 } from "@/components/ui/Headers"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; import { ClusterInfo } from "./common/ClusterInfo"; -import { RelativeValuesDefinitionRevision$key } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; - -export const RelativeValuesDefinitionRevisionFragment = graphql` - fragment RelativeValuesDefinitionRevision on RelativeValuesDefinitionRevision { - title - clusters { - id - color - recommendedUnit - } - items { - id - name - description - clusterId - } - recommendedUnit - } -`; - type Props = { - dataRef: RelativeValuesDefinitionRevision$key; + revision: RelativeValuesDefinitionFullDTO["currentRevision"]; }; export const RelativeValuesDefinitionRevision: FC = ({ - dataRef: definitionRef, + revision: content, }) => { - const content = useFragment( - RelativeValuesDefinitionRevisionFragment, - definitionRef - ); - const clusters = Object.fromEntries( content.clusters.map((cluster) => [cluster.id, cluster]) ); diff --git a/packages/hub/src/relative-values/components/views/ListView/index.tsx b/packages/hub/src/relative-values/components/views/ListView/index.tsx index 1b6c75a336..e7ec94c852 100644 --- a/packages/hub/src/relative-values/components/views/ListView/index.tsx +++ b/packages/hub/src/relative-values/components/views/ListView/index.tsx @@ -19,11 +19,9 @@ import { import { ColumnHeader } from "./ColumnHeader"; import { ItemSideBar } from "./sidebar"; -import { RelativeValuesDefinitionRevision$data } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; - type TableProps = { model: ModelEvaluator; - items: RelativeValuesDefinitionRevision$data["items"]; + items: Item[]; showDescriptions: boolean; recommendedUnit: Item; sidebarItems: [Item, Item] | undefined; diff --git a/packages/hub/src/relative-values/components/views/PlotView/ForcePlot.tsx b/packages/hub/src/relative-values/components/views/PlotView/ForcePlot.tsx index 80ff04f6df..1f3588ca63 100644 --- a/packages/hub/src/relative-values/components/views/PlotView/ForcePlot.tsx +++ b/packages/hub/src/relative-values/components/views/PlotView/ForcePlot.tsx @@ -47,7 +47,7 @@ export const ForcePlot: FC<{ id: string; x: number; y: number; - clusterId: string | null; + clusterId: string | null | undefined; name: string; }; diff --git a/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx b/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx index f6a04eb400..4170a9e641 100644 --- a/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx +++ b/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx @@ -3,12 +3,11 @@ import { FC, PropsWithChildren, Reducer } from "react"; import { generateProvider } from "@quri/ui"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; import { Filter } from "./types"; -import { RelativeValuesDefinitionRevision$data } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; - export type Axis = "rows" | "columns"; export type GridMode = "full" | "half"; @@ -27,7 +26,7 @@ export type AxisConfig = { type ViewContextShape = { evaluator: ModelEvaluator; - definition: RelativeValuesDefinitionRevision$data; + definition: RelativeValuesDefinitionFullDTO["currentRevision"]; gridMode: GridMode; axisConfig: { [k in Axis]: AxisConfig }; }; @@ -167,7 +166,7 @@ const { export const RelativeValuesProvider: FC< PropsWithChildren<{ - definition: RelativeValuesDefinitionRevision$data; + definition: RelativeValuesDefinitionFullDTO["currentRevision"]; evaluator: ModelEvaluator; }> > = ({ evaluator, definition, children }) => { diff --git a/packages/hub/src/relative-values/components/views/hooks/useFilteredItems.ts b/packages/hub/src/relative-values/components/views/hooks/useFilteredItems.ts index 1e29b4a446..d234a67b54 100644 --- a/packages/hub/src/relative-values/components/views/hooks/useFilteredItems.ts +++ b/packages/hub/src/relative-values/components/views/hooks/useFilteredItems.ts @@ -1,14 +1,14 @@ import { useMemo } from "react"; -import { AxisConfig } from "../RelativeValuesProvider"; +import { Item } from "@/relative-values/types"; -import { RelativeValuesDefinitionRevision$data } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; +import { AxisConfig } from "../RelativeValuesProvider"; export const useFilteredItems = ({ items, config, }: { - items: RelativeValuesDefinitionRevision$data["items"]; + items: Item[]; config: AxisConfig; }) => { return useMemo(() => { diff --git a/packages/hub/src/relative-values/data/cards.ts b/packages/hub/src/relative-values/data/cards.ts new file mode 100644 index 0000000000..aff0f63b13 --- /dev/null +++ b/packages/hub/src/relative-values/data/cards.ts @@ -0,0 +1,105 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; +import { + selectTypedOwner, + toTypedOwnerDTO, + TypedOwner, +} from "@/owners/data/typedOwner"; + +export const definitionCardSelect = { + id: true, + slug: true, + updatedAt: true, + owner: { + select: selectTypedOwner, + }, +} satisfies Prisma.RelativeValuesDefinitionSelect; + +type DbDefinitionCard = NonNullable< + Awaited< + ReturnType< + typeof prisma.relativeValuesDefinition.findFirst<{ + select: typeof definitionCardSelect; + }> + > + > +>; + +type DefinitionCardDTO = { + id: string; + slug: string; + owner: TypedOwner; + updatedAt: Date; +}; + +export function toDefinitionCardDTO( + dbDefinition: DbDefinitionCard +): DefinitionCardDTO { + return { + id: dbDefinition.id, + slug: dbDefinition.slug, + owner: toTypedOwnerDTO(dbDefinition.owner), + updatedAt: dbDefinition.updatedAt, + }; +} + +export type RelativeValuesDefinitionCardDTO = ReturnType< + typeof toDefinitionCardDTO +>; + +export async function loadDefinitionCards( + params: { + username?: string; + cursor?: string; + limit?: number; + } = {} +): Promise> { + const limit = params.limit ?? 20; + + const dbDefinitions = await prisma.relativeValuesDefinition.findMany({ + select: definitionCardSelect, + orderBy: { updatedAt: "desc" }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: params.username + ? { + owner: { + slug: params.username, + }, + } + : undefined, + take: limit + 1, + }); + + const definitions = dbDefinitions.map(toDefinitionCardDTO); + + const nextCursor = definitions[definitions.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadDefinitionCards({ ...params, cursor: nextCursor, limit }); + } + + return { + items: definitions.slice(0, limit), + loadMore: definitions.length > limit ? loadMore : undefined, + }; +} + +export async function loadRelativeValuesDefinitionCard(params: { + owner: string; + slug: string; +}): Promise { + const dbDefinition = await prisma.relativeValuesDefinition.findFirst({ + select: definitionCardSelect, + where: { + owner: { + slug: params.owner, + }, + slug: params.slug, + }, + }); + + return dbDefinition ? toDefinitionCardDTO(dbDefinition) : null; +} diff --git a/packages/hub/src/relative-values/data/exports.ts b/packages/hub/src/relative-values/data/exports.ts new file mode 100644 index 0000000000..d08babc902 --- /dev/null +++ b/packages/hub/src/relative-values/data/exports.ts @@ -0,0 +1,83 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; + +import { RelativeValuesDefinitionFullDTO } from "./full"; + +const select = { + id: true, + variableName: true, + modelRevision: { + select: { + model: { + select: { + slug: true, + isPrivate: true, + owner: { select: { slug: true } }, + }, + }, + }, + }, +} satisfies Prisma.RelativeValuesExportSelect; + +export type RelativeValuesExportCardDTO = { + id: string; + variableName: string; + modelRevision: { + model: { + slug: string; + isPrivate: boolean; + owner: { slug: string }; + }; + }; +}; + +function toDTO( + row: Prisma.RelativeValuesExportGetPayload<{ + select: typeof select; + }> +): RelativeValuesExportCardDTO { + return { + id: row.id, + variableName: row.variableName, + modelRevision: { + model: { + slug: row.modelRevision.model.slug, + isPrivate: row.modelRevision.model.isPrivate, + owner: { slug: row.modelRevision.model.owner.slug }, + }, + }, + }; +} + +export async function loadRelativeValuesExportCardsFromDefinition( + definition: RelativeValuesDefinitionFullDTO +): Promise { + const models = await prisma.model.findMany({ + where: { + currentRevision: { + relativeValuesExports: { + some: { + definitionId: definition.id, + }, + }, + }, + OR: await modelWhereHasAccess(), + }, + }); + + const rows = await prisma.relativeValuesExport.findMany({ + where: { + modelRevisionId: { + in: models + .map((model) => model.currentRevisionId) + .filter((id) => id !== null), + }, + definitionId: definition.id, + }, + select, + }); + + return rows.map(toDTO); +} diff --git a/packages/hub/src/relative-values/data/findRelativeValuesForSelect.ts b/packages/hub/src/relative-values/data/findRelativeValuesForSelect.ts new file mode 100644 index 0000000000..e162c474f3 --- /dev/null +++ b/packages/hub/src/relative-values/data/findRelativeValuesForSelect.ts @@ -0,0 +1,31 @@ +import { prisma } from "@/lib/server/prisma"; + +export type FindRelativeValuesForSelectResult = { + id: string; + slug: string; +}; + +export async function findRelativeValuesForSelect({ + owner, + slugContains, +}: { + owner: string; + slugContains: string; +}): Promise { + const rows = await prisma.relativeValuesDefinition.findMany({ + where: { + slug: { + contains: slugContains, + mode: "insensitive", + }, + owner: { slug: owner }, + }, + orderBy: { updatedAt: "desc" }, + take: 20, + }); + + return rows.map((row) => ({ + id: row.id, + slug: row.slug, + })); +} diff --git a/packages/hub/src/relative-values/data/full.ts b/packages/hub/src/relative-values/data/full.ts new file mode 100644 index 0000000000..b9212f6848 --- /dev/null +++ b/packages/hub/src/relative-values/data/full.ts @@ -0,0 +1,101 @@ +import { Prisma } from "@prisma/client"; +import { z } from "zod"; + +import { prisma } from "@/lib/server/prisma"; +import { + relativeValuesClustersSchema, + relativeValuesItemsSchema, +} from "@/relative-values/types"; + +import { + definitionCardSelect, + RelativeValuesDefinitionCardDTO, + toDefinitionCardDTO, +} from "./cards"; + +const select = { + ...definitionCardSelect, + currentRevision: { + select: { + id: true, + title: true, + items: true, + clusters: true, + recommendedUnit: true, + }, + }, +} satisfies Prisma.RelativeValuesDefinitionSelect; + +type DbDefinitionFull = NonNullable< + Awaited< + ReturnType< + typeof prisma.relativeValuesDefinition.findFirst<{ + select: typeof select; + }> + > + > +>; + +type NoUndefinedInObjectList = { + [K in keyof T[number]]-?: Exclude; +}[]; + +export type RelativeValuesDefinitionFullDTO = + RelativeValuesDefinitionCardDTO & { + currentRevision: { + id: string; + title: string; + items: NoUndefinedInObjectList>; + clusters: NoUndefinedInObjectList< + z.infer + >; + recommendedUnit: string | null; + }; + }; + +function toDefinitionFullDTO( + row: DbDefinitionFull +): RelativeValuesDefinitionFullDTO { + if (!row.currentRevision) { + // should never happen + throw new Error("Current revision is required"); + } + + return { + ...toDefinitionCardDTO(row), + currentRevision: { + id: row.currentRevision.id, + title: row.currentRevision.title, + items: relativeValuesItemsSchema + .parse(row.currentRevision.items) + .map((item) => ({ + ...item, + clusterId: item.clusterId ?? null, + })), + clusters: relativeValuesClustersSchema + .parse(row.currentRevision.clusters) + .map((cluster) => ({ + ...cluster, + recommendedUnit: cluster.recommendedUnit ?? null, + })), + recommendedUnit: row.currentRevision.recommendedUnit, + }, + }; +} + +export async function loadRelativeValuesDefinitionFull(params: { + owner: string; + slug: string; +}): Promise { + const row = await prisma.relativeValuesDefinition.findFirst({ + where: { + owner: { slug: params.owner }, + slug: params.slug, + }, + select, + }); + if (!row) { + return null; + } + return toDefinitionFullDTO(row); +} diff --git a/packages/hub/src/relative-values/data/fullExport.ts b/packages/hub/src/relative-values/data/fullExport.ts new file mode 100644 index 0000000000..aacfa72ca3 --- /dev/null +++ b/packages/hub/src/relative-values/data/fullExport.ts @@ -0,0 +1,106 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; + +const select = { + id: true, + variableName: true, + modelRevision: { + select: { + model: { + select: { + slug: true, + isPrivate: true, + owner: { select: { slug: true } }, + }, + }, + }, + }, + cache: { + select: { + firstItem: true, + secondItem: true, + result: true, + error: true, + }, + }, + definition: { + select: { + owner: { select: { slug: true } }, + slug: true, + }, + }, +} satisfies Prisma.RelativeValuesExportSelect; + +export type RelativeValuesExportFullDTO = { + id: string; + variableName: string; + modelRevision: { + model: { + slug: string; + isPrivate: boolean; + owner: { slug: string }; + }; + }; + cache: { + firstItem: string; + secondItem: string; + resultJSON: string; + errorString: string | null; + }[]; + definition: { + owner: string; + slug: string; + }; +}; + +function toDTO( + row: Prisma.RelativeValuesExportGetPayload<{ + select: typeof select; + }> +): RelativeValuesExportFullDTO { + return { + id: row.id, + variableName: row.variableName, + modelRevision: { + model: { + slug: row.modelRevision.model.slug, + isPrivate: row.modelRevision.model.isPrivate, + owner: { slug: row.modelRevision.model.owner.slug }, + }, + }, + cache: row.cache.map((c) => ({ + firstItem: c.firstItem, + secondItem: c.secondItem, + resultJSON: JSON.stringify(c.result), + errorString: c.error, + })), + definition: { + owner: row.definition.owner.slug, + slug: row.definition.slug, + }, + }; +} + +export async function loadRelativeValuesExportFullFromModelRevision(params: { + modelRevisionId: string; + variableName: string; +}): Promise { + const exports = await prisma.relativeValuesExport.findMany({ + where: { + modelRevisionId: params.modelRevisionId, + variableName: params.variableName, + }, + select, + }); + + if (exports.length > 1) { + throw new Error("Ambiguous input, multiple variables match it"); + } + + if (exports.length === 0) { + return null; + } + + return toDTO(exports[0]); +} diff --git a/packages/hub/src/relative-values/types.ts b/packages/hub/src/relative-values/types.ts index f12ec64255..e66566b479 100644 --- a/packages/hub/src/relative-values/types.ts +++ b/packages/hub/src/relative-values/types.ts @@ -1,17 +1,5 @@ import { z } from "zod"; -import { RelativeValuesDefinitionRevision$data } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; - -export type Cluster = RelativeValuesDefinitionRevision$data["clusters"][0]; - -// TODO - should be Map -export type Clusters = { - [k: string]: Cluster; -}; - -export type Item = RelativeValuesDefinitionRevision$data["items"][0]; - -// used in GraphQL schema export const relativeValuesClustersSchema = z.array( z.object({ id: z.string(), @@ -28,3 +16,12 @@ export const relativeValuesItemsSchema = z.array( description: z.string().default(""), }) ); + +export type Cluster = z.infer[number]; + +// TODO - should be Map +export type Clusters = { + [k: string]: Cluster; +}; + +export type Item = z.infer[number]; diff --git a/packages/hub/src/graphql/types/RelativeValuesExport.ts b/packages/hub/src/relative-values/utils.ts similarity index 50% rename from packages/hub/src/graphql/types/RelativeValuesExport.ts rename to packages/hub/src/relative-values/utils.ts index 2d70ebb9ab..0890ce5550 100644 --- a/packages/hub/src/graphql/types/RelativeValuesExport.ts +++ b/packages/hub/src/relative-values/utils.ts @@ -1,16 +1,10 @@ -import { Session } from "next-auth"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableModel } from "../helpers/modelHelpers"; +import { prisma } from "@/lib/server/prisma"; +import { getWriteableModel } from "@/models/utils"; export async function getRelativeValuesExportForWriteableModel({ exportId, - session, }: { exportId: string; - session: Session; }) { const relativeValuesExport = await prisma.relativeValuesExport.findUniqueOrThrow({ @@ -38,37 +32,9 @@ export async function getRelativeValuesExportForWriteableModel({ // checking permissions await getWriteableModel({ - session, owner: relativeValuesExport.modelRevision.model.owner.slug, slug: relativeValuesExport.modelRevision.model.slug, }); return relativeValuesExport; } - -export const RelativeValuesPairCache = builder.prismaNode( - "RelativeValuesPairCache", - { - id: { field: "id" }, - fields: (t) => ({ - firstItem: t.exposeString("firstItem"), - secondItem: t.exposeString("secondItem"), - resultJSON: t.string({ - resolve(obj) { - return JSON.stringify(obj.result); - }, - }), - errorString: t.exposeString("error", { nullable: true }), - }), - } -); - -export const RelativeValuesExport = builder.prismaNode("RelativeValuesExport", { - id: { field: "id" }, - fields: (t) => ({ - definition: t.relation("definition"), - modelRevision: t.relation("modelRevision"), - variableName: t.exposeString("variableName"), - cache: t.relation("cache"), - }), -}); diff --git a/packages/hub/src/relative-values/values/ModelEvaluator.ts b/packages/hub/src/relative-values/values/ModelEvaluator.ts index d414769624..6b427dca7b 100644 --- a/packages/hub/src/relative-values/values/ModelEvaluator.ts +++ b/packages/hub/src/relative-values/values/ModelEvaluator.ts @@ -8,6 +8,8 @@ import { SqStringValue, } from "@quri/squiggle-lang"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; + import { cartesianProduct } from "../lib/utils"; import { RelativeValue, @@ -16,8 +18,6 @@ import { relativeValueSchema, } from "./types"; -import { RelativeValuesExport$data } from "@/__generated__/RelativeValuesExport.graphql"; - export const extractOkValues = (items: result[]): A[] => { return items .filter((item): item is { ok: true; value: A } => item.ok) @@ -101,9 +101,10 @@ export class ModelEvaluator { static async create( modelCode: string, variableName: string, - cache?: RelativeValuesExport$data["cache"] + cache?: RelativeValuesExportFullDTO["cache"] ): Promise> { // TODO - versioned-components + // TODO - support hub imports const project = new SqProject({ linker: makeSelfContainedLinker({ wrapper: ` diff --git a/packages/hub/src/relay/PatchedQueryResponseCache.ts b/packages/hub/src/relay/PatchedQueryResponseCache.ts deleted file mode 100644 index fbb244d2d2..0000000000 --- a/packages/hub/src/relay/PatchedQueryResponseCache.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -// Patched for Squiggle Hub with `delete` method and converted to Typescript. - -import invariant from "invariant"; -import { - GraphQLResponse, - GraphQLSingularResponse, - Variables, -} from "relay-runtime"; - -type Response = { - fetchTime: number; - payload: GraphQLResponse; -}; - -/** - * Creates a copy of the provided value, ensuring any nested objects have their - * keys sorted such that equivalent values would have identical JSON.stringify - * results. - */ -function stableCopy(value: unknown): unknown { - if (!value || typeof value !== "object") { - return value; - } - if (Array.isArray(value)) { - return value.map(stableCopy); - } - const keys = Object.keys(value).sort(); - const stable: Record = {}; - for (let i = 0; i < keys.length; i++) { - stable[keys[i]] = stableCopy((value as Record)[keys[i]]); - } - return stable; -} - -/** - * A cache for storing query responses, featuring: - * - `get` with TTL - * - cache size limiting, with least-recently *updated* entries purged first - */ -export class PatchedQueryResponseCache { - _responses: Map; - _size: number; - _ttl: number; - - constructor({ size, ttl }: { size: number; ttl: number }) { - invariant( - size > 0, - "RelayQueryResponseCache: Expected the max cache size to be > 0, got " + - "`%s`.", - size - ); - invariant( - ttl > 0, - "RelayQueryResponseCache: Expected the max ttl to be > 0, got `%s`.", - ttl - ); - this._responses = new Map(); - this._size = size; - this._ttl = ttl; - } - - clear(): void { - this._responses.clear(); - } - - get(queryID: string, variables: Variables): GraphQLResponse | null { - const cacheKey = getCacheKey(queryID, variables); - this._responses.forEach((response, key) => { - if (!isCurrent(response.fetchTime, this._ttl)) { - this._responses.delete(key); - } - }); - const response = this._responses.get(cacheKey); - if (response == null) { - return null; - } - - const { payload } = response; - - if (Array.isArray(payload)) { - return payload.map( - (payloadItem) => - ({ - ...payloadItem, - extensions: { - ...payloadItem.extensions, - cacheTimestamp: response.fetchTime, - }, - }) satisfies GraphQLSingularResponse - ); - } - - const singularPayload = payload as GraphQLSingularResponse; - - return { - ...singularPayload, - extensions: { - ...singularPayload.extensions, - cacheTimestamp: response.fetchTime, - }, - } satisfies GraphQLSingularResponse; - } - - set(queryID: string, variables: Variables, payload: GraphQLResponse): void { - const fetchTime = Date.now(); - const cacheKey = getCacheKey(queryID, variables); - this._responses.delete(cacheKey); // deletion resets key ordering - this._responses.set(cacheKey, { - fetchTime, - payload, - }); - // Purge least-recently updated key when max size reached - if (this._responses.size > this._size) { - const firstKey = this._responses.keys().next(); - if (!firstKey.done) { - this._responses.delete(firstKey.value); - } - } - } - - delete(queryID: string, variables: Variables) { - const cacheKey = getCacheKey(queryID, variables); - this._responses.delete(cacheKey); - } -} - -function getCacheKey(queryID: string, variables: Variables): string { - return JSON.stringify(stableCopy({ queryID, variables })); -} - -/** - * Determine whether a response fetched at `fetchTime` is still valid given - * some `ttl`. - */ -function isCurrent(fetchTime: number, ttl: number): boolean { - return fetchTime + ttl >= Date.now(); -} diff --git a/packages/hub/src/relay/environment.ts b/packages/hub/src/relay/environment.ts deleted file mode 100644 index 6a207e4ce2..0000000000 --- a/packages/hub/src/relay/environment.ts +++ /dev/null @@ -1,121 +0,0 @@ -// based on https://github.com/relayjs/relay-examples/blob/main/issue-tracker-next-v13/src/relay/environment.ts -import { - CacheConfig, - Environment, - EnvironmentConfig, - GraphQLResponse, - Network, - RecordSource, - RequestParameters, - Store, - Variables, -} from "relay-runtime"; - -import { VERCEL_URL } from "@/constants"; - -import { PatchedQueryResponseCache } from "./PatchedQueryResponseCache"; - -const IS_SERVER = typeof window === typeof undefined; - -const HTTP_ENDPOINT = IS_SERVER - ? VERCEL_URL - ? `https://${VERCEL_URL}/api/graphql` - : "http://localhost:3001/api/graphql" - : "/api/graphql"; - -const CACHE_TTL = 5 * 1000; // 5 seconds, to resolve preloaded results - -export async function networkFetch( - request: RequestParameters, - variables: Variables, - cookieHeader?: string -): Promise { - const resp = await fetch(HTTP_ENDPOINT, { - cache: "no-store", - method: "POST", - headers: { - Cookie: cookieHeader ?? "", - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: request.text, - variables, - }), - }); - const json = await resp.json(); - - // GraphQL returns exceptions (for example, a missing required variable) in the "errors" - // property of the response. If any exceptions occurred when processing the request, - // throw an error to indicate to the developer what went wrong. - if (Array.isArray(json.errors)) { - console.error(json.errors); - throw new Error( - `Error fetching GraphQL query '${ - request.name - }' with variables '${JSON.stringify(variables)}': ${JSON.stringify( - json.errors - )}` - ); - } - - return json; -} - -export function createNetwork(responseCache: PatchedQueryResponseCache) { - const fetchResponse = async ( - params: RequestParameters, - variables: Variables, - cacheConfig: CacheConfig - ): Promise => { - const isQuery = params.operationKind === "query"; - const cacheKey = params.id ?? params.cacheID; - if (isQuery && !cacheConfig.force) { - const fromCache = responseCache.get(cacheKey, variables); - if (fromCache) { - return fromCache; - } - } - - // TODO - on server, it might be better to run GraphQL query directly, instead of going through yoga and HTTP - return await networkFetch(params, variables); - }; - - return Network.create(fetchResponse); -} - -export class EnvironmentWithResponseCache extends Environment { - constructor( - config: EnvironmentConfig, - public responseCache: PatchedQueryResponseCache - ) { - super(config); - } -} - -function createEnvironment() { - // We have response cache even on server; isolation is guaranteed by `getCurrentEnvironment()` logic. - // We expose it as `environment.responseCache` so that `useSerializablePreloadedQuery` could access it. - const responseCache = new PatchedQueryResponseCache({ - size: 100, - ttl: CACHE_TTL, - }); - return new EnvironmentWithResponseCache( - { - network: createNetwork(responseCache), - store: new Store(RecordSource.create()), - isServer: IS_SERVER, - }, - responseCache - ); -} - -let environment: EnvironmentWithResponseCache | undefined; -export function getCurrentEnvironment() { - if (IS_SERVER) { - return createEnvironment(); - } - - environment ??= createEnvironment(); - return environment; -} diff --git a/packages/hub/src/relay/loadPageQuery.ts b/packages/hub/src/relay/loadPageQuery.ts deleted file mode 100644 index 938c4d0cc7..0000000000 --- a/packages/hub/src/relay/loadPageQuery.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { headers } from "next/headers"; -// based on https://github.com/relayjs/relay-examples/blob/main/issue-tracker-next-v13/src/relay/loadSerializableQuery.ts -import { - GraphQLResponse, - OperationType, - RequestParameters, - VariablesOf, -} from "relay-runtime"; -import { ConcreteRequest } from "relay-runtime/lib/util/RelayConcreteNode"; - -import { networkFetch } from "./environment"; - -export interface SerializablePreloadedQuery { - params: RequestParameters; - variables: VariablesOf; - response: GraphQLResponse; -} - -// Call into raw network fetch to get serializable GraphQL query response -// This response will be sent to the client to "warm" the QueryResponseCache -// to avoid the client fetches. -export async function loadPageQuery( - node: ConcreteRequest, - variables: VariablesOf -): Promise> { - const cookie = (await headers()).get("cookie"); - const response = await networkFetch( - node.params, - variables, - cookie ?? undefined - ); - return { - params: node.params, - variables, - response, - }; -} diff --git a/packages/hub/src/relay/usePageQuery.ts b/packages/hub/src/relay/usePageQuery.ts deleted file mode 100644 index 0f3ec8faec..0000000000 --- a/packages/hub/src/relay/usePageQuery.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Convert preloaded query object (with raw GraphQL Response) into Relay's PreloadedQuery. - -import { useEffect, useMemo } from "react"; -import { - PreloadedQuery, - usePreloadedQuery, - useQueryLoader, - useRelayEnvironment, -} from "react-relay"; -import { GraphQLTaggedNode, OperationType } from "relay-runtime"; - -import { EnvironmentWithResponseCache } from "./environment"; -import { SerializablePreloadedQuery } from "./loadPageQuery"; - -// This hook convert serializable preloaded query into Relay's PreloadedQuery object. -// It also writes this serializable preloaded query into QueryResponseCache, so that -// the network layer can use these cache results when fetching data in `usePreloadedQuery`. -// -// In addition to the basic implementation from https://github.com/relayjs/relay-examples/tree/main/issue-tracker-next-v13, this hook: -// - combines `useSerializablePreloadedQuery` and `usePreloadedQuery` into one call -// - returns a `reload()` function that can be used together with `useSubscribeToInvalidationState` to reload page data. -export function usePageQuery( - Query: GraphQLTaggedNode, - rscQueryRef: SerializablePreloadedQuery -) { - const environment = useRelayEnvironment() as EnvironmentWithResponseCache; - - const [initialQueryRef, fetchKey] = useMemo(() => { - const fetchKey = rscQueryRef.params.id ?? rscQueryRef.params.cacheID; - - environment.responseCache.set( - fetchKey, - rscQueryRef.variables, - rscQueryRef.response - ); - - const preloadedQuery = { - environment, - fetchKey, - // On the first page load, it will hit responseCache. - // On the following loads (on client-side navigation), responseCache will be cleaned up when the component is unmounted, - // and the page data will be reloaded. - // Note: in dev, this will cause /api/graphql refetch on the client side, because of React strict mode. But it shouldn't do that in production. - fetchPolicy: "store-and-network", - isDisposed: false, - name: rscQueryRef.params.name, - kind: "PreloadedQuery", - variables: rscQueryRef.variables, - dispose: () => {}, - } as PreloadedQuery; - - return [preloadedQuery, fetchKey]; - }, [rscQueryRef, environment]); - - useEffect(() => { - return () => { - environment.responseCache.delete(fetchKey, rscQueryRef.variables); - }; - }, [fetchKey, environment, initialQueryRef, rscQueryRef.variables]); - - const [queryRef, loadQuery] = useQueryLoader(Query); - const result = usePreloadedQuery(Query, queryRef ?? initialQueryRef); - - const reload = () => loadQuery(rscQueryRef.variables); - - useEffect(() => { - return () => { - initialQueryRef.dispose(); - }; - }, [initialQueryRef]); - - return [result, { reload }] as const; -} diff --git a/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts b/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts new file mode 100644 index 0000000000..218b112227 --- /dev/null +++ b/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts @@ -0,0 +1,47 @@ +import { prisma } from "@/lib/server/prisma"; + +import { type VariableRevisionInput } from "./worker"; + +export async function createVariableRevision( + modelId: string, + revisionId: string, + variableData: VariableRevisionInput +) { + const variable = await prisma.variable.findFirst({ + where: { + modelId: modelId, + variableName: variableData.variableName, + }, + }); + + let variableId: string; + if (!variable) { + const createdVariable = await prisma.variable.create({ + data: { + model: { connect: { id: modelId } }, + variableName: variableData.variableName, + }, + }); + variableId = createdVariable.id; + } else { + variableId = variable.id; + } + + const createdVariableRevision = await prisma.variableRevision.create({ + data: { + variableName: variableData.variableName, + variable: { connect: { id: variableId } }, + modelRevision: { connect: { id: revisionId } }, + variableType: variableData.variableType, + title: variableData.title, + docstring: variableData.docstring, + }, + }); + + await prisma.variable.update({ + where: { id: variableId }, + data: { + currentRevisionId: createdVariableRevision.id, + }, + }); +} diff --git a/packages/hub/src/scripts/buildRecentModelRevision/main.ts b/packages/hub/src/scripts/buildRecentModelRevision/main.ts index bfcc6e6771..46a78fb84e 100644 --- a/packages/hub/src/scripts/buildRecentModelRevision/main.ts +++ b/packages/hub/src/scripts/buildRecentModelRevision/main.ts @@ -1,9 +1,7 @@ import { PrismaClient } from "@prisma/client"; import { spawn } from "node:child_process"; -import { createVariableRevision } from "@/graphql/types/VariableRevision"; - -import { NotFoundError } from "../../graphql/errors/NotFoundError"; +import { createVariableRevision } from "./createVariableRevision"; import { WorkerOutput, WorkerRunMessage } from "./worker"; const TIMEOUT_SECONDS = 60; // 60 seconds @@ -18,7 +16,7 @@ async function runWorker( ): Promise { return new Promise((resolve, _) => { console.log("Spawning worker process for Revision ID: " + revisionId); - const worker = spawn("node", [__dirname + "/worker.js"], { + const worker = spawn("node", [__dirname + "/worker.mjs"], { stdio: ["pipe", "pipe", "pipe", "ipc"], }); @@ -104,7 +102,7 @@ async function buildRecentModelVersion(): Promise { } if (!model?.currentRevisionId || !model.currentRevision?.squiggleSnippet) { - throw new NotFoundError( + throw new Error( `Unexpected Error: Model revision didn't have needed information. This should never happen.` ); } diff --git a/packages/hub/src/scripts/buildRecentModelRevision/worker.ts b/packages/hub/src/scripts/buildRecentModelRevision/worker.ts index 5f521721a4..166e0372c2 100644 --- a/packages/hub/src/scripts/buildRecentModelRevision/worker.ts +++ b/packages/hub/src/scripts/buildRecentModelRevision/worker.ts @@ -1,6 +1,12 @@ -import { runSquiggle } from "@/graphql/queries/runSquiggle"; -import { VariableRevisionInput } from "@/graphql/types/VariableRevision"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; +import { runSquiggle } from "@/lib/server/runSquiggle"; + +export type VariableRevisionInput = { + variableName: string; + variableType: string; + title?: string; + docstring: string; +}; export type WorkerRunMessage = { type: "run"; diff --git a/packages/hub/src/scripts/print-schema.ts b/packages/hub/src/scripts/print-schema.ts deleted file mode 100644 index 309405f643..0000000000 --- a/packages/hub/src/scripts/print-schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { writeFileSync } from "fs"; -import { lexicographicSortSchema, printSchema } from "graphql"; -import path from "path"; - -import { schema } from "../graphql/schema"; - -const schemaAsString = printSchema(lexicographicSortSchema(schema)); - -writeFileSync(path.join(__dirname, "../../schema.graphql"), schemaAsString); diff --git a/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts b/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts new file mode 100644 index 0000000000..05035366e2 --- /dev/null +++ b/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts @@ -0,0 +1,17 @@ +"use server"; + +import { z } from "zod"; + +import { actionClient } from "@/lib/server/actionClient"; +import { checkRootUser } from "@/users/auth"; + +import { rebuildSearchableTable } from "../helpers"; + +// Admin-only query for rebuilding the search index +export const adminRebuildSearchIndexAction = actionClient + .schema(z.object({})) + .action(async () => { + await checkRootUser(); + await rebuildSearchableTable(); + return { ok: true }; + }); diff --git a/packages/hub/src/graphql/helpers/searchHelpers.ts b/packages/hub/src/search/helpers.ts similarity index 96% rename from packages/hub/src/graphql/helpers/searchHelpers.ts rename to packages/hub/src/search/helpers.ts index 33de3c6409..ef9291a185 100644 --- a/packages/hub/src/graphql/helpers/searchHelpers.ts +++ b/packages/hub/src/search/helpers.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; export async function rebuildSearchableTable() { await prisma.$transaction(async (tx) => { diff --git a/packages/hub/src/server/helpers.ts b/packages/hub/src/server/helpers.ts deleted file mode 100644 index 812ee52334..0000000000 --- a/packages/hub/src/server/helpers.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Helper functions for server-side React components - * TODO: unify these with `graphql/helpers/*` - * (see https://github.com/quantified-uncertainty/squiggle/issues/3154, we plan to migrate away from GraphQL) - */ -import "server-only"; - -import { redirect } from "next/navigation"; - -import { isRootEmail, isSignedIn } from "@/graphql/helpers/userHelpers"; -import { prisma } from "@/prisma"; - -import { auth } from "../auth"; - -export async function getSessionUserOrRedirect() { - const session = await auth(); - if (!isSignedIn(session)) { - redirect("/api/auth/signin"); // TODO - callbackUrl - } - - return session.user; -} - -export async function checkRootUser() { - // TODO - unify with src/graphql/helpers - const sessionUser = await getSessionUserOrRedirect(); - const user = await prisma.user.findUniqueOrThrow({ - where: { email: sessionUser.email }, - }); - if (!(user.email && user.emailVerified && isRootEmail(user.email))) { - throw new Error("Unauthorized"); - } -} diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index a8e5139029..76e3587b1e 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -1,12 +1,12 @@ import clsx from "clsx"; -import { FC } from "react"; -import { graphql, useLazyLoadQuery } from "react-relay"; +import { FC, useEffect, useState } from "react"; +import Skeleton from "react-loading-skeleton"; +import { loadModelCardAction } from "@/models/actions/loadModelCardAction"; import { ModelCard } from "@/models/components/ModelCard"; +import { ModelCardDTO } from "@/models/data/cards"; -import { parseSourceId } from "./linker"; - -import { ImportTooltipQuery } from "@/__generated__/ImportTooltipQuery.graphql"; +import { parseSourceId } from "../linker"; type Props = { importId: string; @@ -15,28 +15,22 @@ type Props = { export const ImportTooltip: FC = ({ importId }) => { const { owner, slug } = parseSourceId(importId); - const { model } = useLazyLoadQuery( - graphql` - query ImportTooltipQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - ...ModelCard - } - } - } - `, - { input: { owner, slug } } + const [model, setModel] = useState( + "loading" ); - if (model.__typename !== "Model") { - return ( -
- {"Couldn't load "} - {owner}/{slug} -
- ); - } + useEffect(() => { + // TODO - this is done with a server action, so it's not cached. + // A route would be better. + loadModelCardAction({ owner, slug }).then((result) => { + if (result?.data) { + setModel(result.data); + } else { + // TODO - handle errors + setModel(null); + } + }); + }, [owner, slug]); return (
= ({ importId }) => { "text-base" )} > - + {model === "loading" ? ( + + ) : model ? ( + + ) : ( +
+ Model not found. +
+ )}
); }; diff --git a/packages/hub/src/squiggle/components/ViewSquiggleSnippet.tsx b/packages/hub/src/squiggle/components/ViewSquiggleSnippet.tsx deleted file mode 100644 index 0fee27e165..0000000000 --- a/packages/hub/src/squiggle/components/ViewSquiggleSnippet.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { FC, use, useMemo } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { - useAdjustSquiggleVersion, - versionedSquigglePackages, -} from "@quri/versioned-squiggle-components"; - -import { sqProjectWithHubLinker } from "./linker"; - -import { ViewSquiggleSnippet$key } from "@/__generated__/ViewSquiggleSnippet.graphql"; - -type Props = { - dataRef: ViewSquiggleSnippet$key; -}; - -export const ViewSquiggleSnippet: FC = ({ dataRef }) => { - const { version, code } = useFragment( - graphql` - fragment ViewSquiggleSnippet on SquiggleSnippet { - id - code - version - } - `, - dataRef - ); - - const checkedVersion = useAdjustSquiggleVersion(version); - - const squiggle = use(versionedSquigglePackages(checkedVersion)); - - const project = useMemo(() => sqProjectWithHubLinker(squiggle), [squiggle]); - - return ; -}; diff --git a/packages/hub/src/squiggle/components/linker.ts b/packages/hub/src/squiggle/linker.ts similarity index 57% rename from packages/hub/src/squiggle/components/linker.ts rename to packages/hub/src/squiggle/linker.ts index 766c471f85..b4ea7098f1 100644 --- a/packages/hub/src/squiggle/components/linker.ts +++ b/packages/hub/src/squiggle/linker.ts @@ -1,13 +1,10 @@ -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SqLinker, SqModule } from "@quri/squiggle-lang"; -import { versionSupportsSqProjectV2 } from "@quri/versioned-squiggle-components"; - -import { getCurrentEnvironment } from "@/relay/environment"; - -import { versionedSquigglePackages } from "../../../../versioned-components/dist/src/versionedSquigglePackages"; - -import { linkerQuery } from "@/__generated__/linkerQuery.graphql"; +import { + versionedSquigglePackages, + versionSupportsSqProjectV2, +} from "@quri/versioned-squiggle-components"; type ParsedSourceId = { owner: string; @@ -45,46 +42,18 @@ const linker: SqLinker = { async loadModule(sourceId: string) { const { owner, slug } = parseSourceId(sourceId); - const environment = getCurrentEnvironment(); - - const result = await fetchQuery( - environment, - graphql` - query linkerQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - id - currentRevision { - content { - __typename - ... on SquiggleSnippet { - code - } - } - } - } - } - } - `, - { - input: { owner, slug }, - } - // toPromise is discouraged by Relay docs, but should be fine if we don't do any streaming - ).toPromise(); - - if (!result || result.model.__typename !== "Model") { - throw new Error(`Failed to fetch sources for ${sourceId}`); - } + const data = await fetch( + `/api/get-source?${new URLSearchParams({ owner, slug })}` + ).then((res) => res.json()); - const content = result.model.currentRevision.content; - if (content.__typename !== "SquiggleSnippet") { - throw new Error(`${sourceId} is not a SquiggleSnippet`); + const parsed = z.object({ code: z.string() }).safeParse(data); + if (!parsed.success) { + throw new Error(`Failed to fetch source for ${sourceId}`); } return new SqModule({ name: sourceId, - code: content.code, + code: parsed.data.code, }); }, }; diff --git a/packages/hub/src/users/actions/setUsernameAction.ts b/packages/hub/src/users/actions/setUsernameAction.ts new file mode 100644 index 0000000000..98a6767b11 --- /dev/null +++ b/packages/hub/src/users/actions/setUsernameAction.ts @@ -0,0 +1,48 @@ +"use server"; + +import { returnValidationErrors } from "next-safe-action"; +import { z } from "zod"; + +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; + +const schema = z.object({ + username: z.string().min(1), +}); + +export const setUsernameAction = actionClient + .schema(schema) + .action(async ({ parsedInput: input }): Promise<"ok"> => { + const session = await auth(); + if (!session?.user.email) { + throw new ActionError("Not signed in"); + } + if (session.user.username) { + throw new ActionError("Username is already set"); + } + + const existingOwner = await prisma.owner.count({ + where: { slug: input.username }, + }); + if (existingOwner) { + returnValidationErrors(schema, { + username: { + _errors: [`Username ${input.username} is not available`], + }, + }); + } + + await prisma.user.update({ + where: { + email: session.user.email, + }, + data: { + asOwner: { + create: { slug: input.username }, + }, + }, + }); + + return "ok"; + }); diff --git a/packages/hub/src/users/auth.ts b/packages/hub/src/users/auth.ts new file mode 100644 index 0000000000..3235fb3353 --- /dev/null +++ b/packages/hub/src/users/auth.ts @@ -0,0 +1,64 @@ +import { User } from "@prisma/client"; +import { Session } from "next-auth"; +import { redirect } from "next/navigation"; + +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; + +export async function getSessionOrRedirect() { + const session = await auth(); + if (!isSignedIn(session)) { + redirect("/api/auth/signin"); // TODO - callbackUrl + } + + return session; +} + +export async function getSessionUserOrRedirect() { + return (await getSessionOrRedirect()).user; +} + +// Checks if the user is a root user. If so, returns the user. +export async function checkRootUser() { + const sessionUser = await getSessionUserOrRedirect(); + const user = await prisma.user.findUniqueOrThrow({ + where: { email: sessionUser.email }, + }); + if (!(user.email && user.emailVerified && isRootEmail(user.email))) { + throw new Error("Unauthorized"); + } + return user as User & { email: NonNullable }; +} + +export type SignedInSession = Session & { + user: NonNullable & { + email: NonNullable; + username: NonNullable; + }; +}; + +export function isSignedIn( + session: Session | null +): session is SignedInSession { + // Note: username is not set initially, when the user first signs in. + // `useForceChooseUsername` hook will redirect the user to the choose username page if necessary. + // The server components and server actions involved in that shouldn't rely on this function to check if the user is signed in. + return Boolean(session?.user.email && session.user.username); +} + +export async function getSelf(session: SignedInSession) { + const user = await prisma.user.findUniqueOrThrow({ + where: { email: session.user.email }, + }); + return user; +} + +const ROOT_EMAILS = (process.env["ROOT_EMAILS"] ?? "").split(","); + +export function isRootEmail(email: string) { + return ROOT_EMAILS.includes(email); +} + +export async function isRootUser(user: User) { + return Boolean(user.email && user.emailVerified && isRootEmail(user.email)); +} diff --git a/packages/hub/src/users/data/layoutUser.ts b/packages/hub/src/users/data/layoutUser.ts new file mode 100644 index 0000000000..519c417dff --- /dev/null +++ b/packages/hub/src/users/data/layoutUser.ts @@ -0,0 +1,99 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; + +const getSelect = async () => + ({ + id: true, + asOwner: { + select: { + slug: true, + // count models + models: { + take: 1, + where: { OR: await modelWhereHasAccess() }, + select: { id: true }, + }, + // count definitions + relativeValuesDefinitions: { + take: 1, + select: { id: true }, + }, + }, + }, + // count groups + memberships: { + take: 1, + select: { id: true }, + }, + }) satisfies Prisma.UserSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.user.findFirst<{ + select: Awaited>; + }> + > + > +>; + +export type UserLayoutDTO = { + id: string; + username: string; + hasModels: boolean; + hasDefinitions: boolean; + hasGroups: boolean; + hasVariables: boolean; +}; + +function toDTO( + row: Row, + { hasVariables }: { hasVariables: boolean } +): UserLayoutDTO { + return { + id: row.id, + username: row.asOwner?.slug ?? "", + hasModels: !!row.asOwner?.models.length, + hasDefinitions: !!row.asOwner?.relativeValuesDefinitions?.length, + hasGroups: !!row.memberships.length, + hasVariables, + }; +} + +export async function loadLayoutUser( + username: string +): Promise { + const row = await prisma.user.findFirst({ + where: { + asOwner: { + slug: username, + }, + }, + select: await getSelect(), + }); + + if (!row) { + return null; + } + + const hasVariables = !!(await prisma.variable.findFirst({ + select: { id: true }, + where: { + model: { + // variables from this user's models + owner: { + slug: username, + }, + // that the viewer has access to + OR: await modelWhereHasAccess(), + }, + }, + take: 1, + })); + + return toDTO(row, { + hasVariables, + }); +} diff --git a/packages/hub/src/variables/components/VariableCard.tsx b/packages/hub/src/variables/components/VariableCard.tsx index e5b592db33..29a52f50ab 100644 --- a/packages/hub/src/variables/components/VariableCard.tsx +++ b/packages/hub/src/variables/components/VariableCard.tsx @@ -1,6 +1,4 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { MarkdownViewer } from "@quri/squiggle-components"; import { CodeBracketSquareIcon } from "@quri/ui"; @@ -14,41 +12,15 @@ import { UpdatedStatus, } from "@/components/EntityCard"; import { Link } from "@/components/ui/Link"; +import { modelRoute, variableRoute } from "@/lib/routes"; import { exportTypeIcon } from "@/lib/typeIcon"; -import { modelRoute, variableRoute } from "@/routes"; - -import { VariableCard$key } from "@/__generated__/VariableCard.graphql"; - -const Fragment = graphql` - fragment VariableCard on Variable { - id - variableName - currentRevision { - id - title - docstring - variableType - modelRevision { - createdAtTimestamp - } - } - owner { - slug - } - model { - slug - isPrivate - } - } -`; +import { VariableCardDTO } from "@/variables/data/variableCards"; type Props = { - variableRef: VariableCard$key; + variable: VariableCardDTO; }; -export const VariableCard: FC = ({ variableRef }) => { - const variable = useFragment(Fragment, variableRef); - +export const VariableCard: FC = ({ variable }) => { const currentRevision = variable.currentRevision; if (!currentRevision) { @@ -59,7 +31,7 @@ export const VariableCard: FC = ({ variableRef }) => { // This will have problems with markdown tags, but I looked into markdown-truncation packages, and they can get complicated. Will try this for now. - const { createdAtTimestamp } = currentRevision.modelRevision; + const { createdAt } = currentRevision.modelRevision; return (
@@ -70,7 +42,7 @@ export const VariableCard: FC = ({ variableRef }) => { href={variableRoute({ modelSlug: variable.model.slug, variableName: variable.variableName, - owner: variable.owner.slug, + owner: variable.model.owner.slug, })} > {variable.currentRevision?.title || variable.variableName} @@ -80,12 +52,12 @@ export const VariableCard: FC = ({ variableRef }) => { - {`${variable.owner.slug}/${variable.model.slug}`} + {`${variable.model.owner.slug}/${variable.model.slug}`}
@@ -95,7 +67,7 @@ export const VariableCard: FC = ({ variableRef }) => { {currentRevision.variableType} , - , + , variable.model.isPrivate && , ]} /> diff --git a/packages/hub/src/variables/components/VariableList.tsx b/packages/hub/src/variables/components/VariableList.tsx index 00a7c31168..2fb08a8ff0 100644 --- a/packages/hub/src/variables/components/VariableList.tsx +++ b/packages/hub/src/variables/components/VariableList.tsx @@ -1,43 +1,30 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { LoadMore } from "@/components/LoadMore"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; +import { VariableCardDTO } from "@/variables/data/variableCards"; import { VariableCard } from "./VariableCard"; -import { VariableList$key } from "@/__generated__/VariableList.graphql"; - -const Fragment = graphql` - fragment VariableList on VariableConnection { - edges { - node { - id - ...VariableCard - } - } - pageInfo { - hasNextPage - } - } -`; - type Props = { - connectionRef: VariableList$key; - loadNext(count: number): unknown; + page: Paginated; }; -export const VariableList: FC = ({ connectionRef, loadNext }) => { - const connection = useFragment(Fragment, connectionRef); +export const VariableList: FC = ({ page: initialPage }) => { + const page = usePaginator(initialPage); - return ( + return page.items.length === 0 ? ( +
No variables found.
+ ) : (
- {connection.edges.map((edge) => ( - + {page.items.map((variable) => ( + ))}
- {connection.pageInfo.hasNextPage && } + {page.loadNext && }
); }; diff --git a/packages/hub/src/lib/VariablesDropdown.tsx b/packages/hub/src/variables/components/VariablesDropdown.tsx similarity index 97% rename from packages/hub/src/lib/VariablesDropdown.tsx rename to packages/hub/src/variables/components/VariablesDropdown.tsx index 146191c947..3f0cdffd3b 100644 --- a/packages/hub/src/lib/VariablesDropdown.tsx +++ b/packages/hub/src/variables/components/VariablesDropdown.tsx @@ -8,9 +8,9 @@ import { } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; -import { modelForRelativeValuesExportRoute, variableRoute } from "@/routes"; +import { modelForRelativeValuesExportRoute, variableRoute } from "@/lib/routes"; -import { exportTypeIcon } from "./typeIcon"; +import { exportTypeIcon } from "../../lib/typeIcon"; export type VariableRevision = { title?: string; diff --git a/packages/hub/src/variables/data/fullVariableRevision.ts b/packages/hub/src/variables/data/fullVariableRevision.ts new file mode 100644 index 0000000000..cef237da73 --- /dev/null +++ b/packages/hub/src/variables/data/fullVariableRevision.ts @@ -0,0 +1,74 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; +import { + ModelRevisionFullDTO, + modelRevisionFullToDTO, + selectModelRevisionFull, +} from "@/models/data/fullRevision"; + +const select = { + id: true, + variableType: true, + variableName: true, + modelRevision: { + select: selectModelRevisionFull, + }, +} satisfies Prisma.VariableRevisionSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.variableRevision.findFirst<{ + select: typeof select; + }> + > + > +>; + +export type VariableRevisionFullDTO = { + id: string; + variableType: string; + variableName: string; + modelRevision: ModelRevisionFullDTO; +}; + +async function toDTO(row: Row): Promise { + return { + id: row.id, + variableType: row.variableType, + variableName: row.variableName, + modelRevision: await modelRevisionFullToDTO(row.modelRevision), + }; +} + +export async function loadVariableRevisionFull({ + owner, + slug, + variableName, + revisionId, +}: { + owner: string; + slug: string; + variableName: string; + revisionId: string; +}): Promise { + const row = await prisma.variableRevision.findFirst({ + where: { + id: revisionId, + modelRevision: { + model: { + OR: await modelWhereHasAccess(), + owner: { + slug: owner, + }, + slug, + }, + }, + }, + select, + }); + + return row ? await toDTO(row) : null; +} diff --git a/packages/hub/src/variables/data/variableCards.ts b/packages/hub/src/variables/data/variableCards.ts new file mode 100644 index 0000000000..397840e11a --- /dev/null +++ b/packages/hub/src/variables/data/variableCards.ts @@ -0,0 +1,124 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; + +const variableCardSelect = { + id: true, + variableName: true, + currentRevision: { + select: { + id: true, + title: true, + docstring: true, + variableType: true, + modelRevision: { + select: { + createdAt: true, + }, + }, + }, + }, + model: { + select: { + owner: { + select: { + slug: true, + }, + }, + slug: true, + isPrivate: true, + }, + }, +} satisfies Prisma.VariableSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.variable.findFirst<{ + select: typeof variableCardSelect; + }> + > + > +>; + +export function toDTO(dbVariable: Row) { + // TODO - upgrade owner, at least + return dbVariable; +} + +export type VariableCardDTO = ReturnType; + +export async function loadVariableCards( + params: { + ownerSlug?: string; + cursor?: string; + limit?: number; + } = {} +): Promise> { + const limit = params.limit ?? 20; + + const dbVariables = await prisma.variable.findMany({ + select: variableCardSelect, + orderBy: { + currentRevision: { + modelRevision: { + createdAt: "desc", + }, + }, + }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: { + model: { + OR: await modelWhereHasAccess(), + ...(params.ownerSlug + ? { + owner: { + slug: params.ownerSlug, + }, + } + : undefined), + }, + }, + take: limit + 1, + }); + + const variables = dbVariables.map(toDTO); + + const nextCursor = variables[variables.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadVariableCards({ ...params, cursor: nextCursor, limit }); + } + + return { + items: variables.slice(0, limit), + loadMore: variables.length > limit ? loadMore : undefined, + }; +} + +export async function loadVariableCard({ + owner, + slug, + variableName, +}: { + owner: string; + slug: string; + variableName: string; +}) { + const row = await prisma.variable.findFirst({ + where: { + model: { + OR: await modelWhereHasAccess(), + owner: { slug: owner }, + slug, + }, + variableName, + }, + select: variableCardSelect, + }); + + return row ? toDTO(row) : null; +} diff --git a/packages/hub/src/variables/data/variableRevisions.ts b/packages/hub/src/variables/data/variableRevisions.ts new file mode 100644 index 0000000000..2f2deaa532 --- /dev/null +++ b/packages/hub/src/variables/data/variableRevisions.ts @@ -0,0 +1,88 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; +import { + ModelRevisionDTO, + modelRevisionToDTO, + selectModelRevision, +} from "@/models/data/revisions"; + +const select = { + id: true, + variableType: true, + modelRevision: { + select: selectModelRevision, + }, +} satisfies Prisma.VariableRevisionSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.variableRevision.findFirst<{ + select: typeof select; + }> + > + > +>; + +export type VariableRevisionDTO = { + id: string; + variableType: string; + modelRevision: ModelRevisionDTO; +}; + +function toDTO(row: Row): VariableRevisionDTO { + return { + id: row.id, + variableType: row.variableType, + modelRevision: modelRevisionToDTO(row.modelRevision), + }; +} + +export async function loadVariableRevisions(params: { + owner: string; + slug: string; + variableName: string; + cursor?: string; + limit?: number; +}): Promise> { + const limit = params.limit ?? 20; + + const rows = await prisma.variableRevision.findMany({ + select, + orderBy: { + modelRevision: { + createdAt: "desc", + }, + }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: { + modelRevision: { + model: { + OR: await modelWhereHasAccess(), + owner: { + slug: params.owner, + }, + slug: params.slug, + }, + }, + }, + take: limit + 1, + }); + + const variables = rows.map(toDTO); + + const nextCursor = variables[variables.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadVariableRevisions({ ...params, cursor: nextCursor, limit }); + } + + return { + items: variables.slice(0, limit), + loadMore: variables.length > limit ? loadMore : undefined, + }; +} diff --git a/packages/hub/test/gql-gen/fragment-masking.ts b/packages/hub/test/gql-gen/fragment-masking.ts deleted file mode 100644 index 194340763d..0000000000 --- a/packages/hub/test/gql-gen/fragment-masking.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - DocumentTypeDecoration, - ResultOf, - TypedDocumentNode, -} from "@graphql-typed-document-node/core"; -import { FragmentDefinitionNode } from "graphql"; - -import { Incremental } from "./graphql"; - -export type FragmentType> = TDocumentType extends DocumentTypeDecoration< - infer TType, - any -> - ? [TType] extends [{ ' $fragmentName'?: infer TKey }] - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } - : never - : never - : never; - -// return non-nullable if `fragmentType` is non-nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> -): TType; -// return nullable if `fragmentType` is nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | null | undefined -): TType | null | undefined; -// return array of non-nullable if `fragmentType` is array of non-nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> -): ReadonlyArray; -// return array of nullable if `fragmentType` is array of nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> | null | undefined -): ReadonlyArray | null | undefined; -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | ReadonlyArray>> | null | undefined -): TType | ReadonlyArray | null | undefined { - return fragmentType as any; -} - - -export function makeFragmentData< - F extends DocumentTypeDecoration, - FT extends ResultOf ->(data: FT, _fragment: F): FragmentType { - return data as FragmentType; -} -export function isFragmentReady( - queryNode: DocumentTypeDecoration, - fragmentNode: TypedDocumentNode, - data: FragmentType, any>> | null | undefined -): data is FragmentType { - const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ - ?.deferredFields; - - if (!deferredFields) return true; - - const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; - const fragName = fragDef?.name?.value; - - const fields = (fragName && deferredFields[fragName]) || []; - return fields.length > 0 && fields.every(field => data && field in data); -} diff --git a/packages/hub/test/gql-gen/gql.ts b/packages/hub/test/gql-gen/gql.ts deleted file mode 100644 index 7f6f52bdd7..0000000000 --- a/packages/hub/test/gql-gen/gql.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core"; - -/* eslint-disable */ -import * as types from "./graphql"; - -/** - * Map of all GraphQL operations in the project. - * - * This map has several performance disadvantages: - * 1. It is not tree-shakeable, so it will include all operations in the project. - * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. - * 3. It does not support dead code elimination, so it will add unused operations. - * - * Therefore it is highly recommended to use the babel or swc plugin for production. - */ -const documents = { - "\n query Test_MyMembership($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n myMembership {\n id\n role\n }\n }\n }\n }\n ": types.Test_MyMembershipDocument, - "\n query Test_Memberships($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n memberships {\n edges {\n node {\n id\n role\n user {\n slug\n }\n }\n }\n }\n }\n }\n }\n ": types.Test_MembershipsDocument, - "\n mutation Test_CreateGroup($input: MutationCreateGroupInput!) {\n result: createGroup(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateGroupResult {\n group {\n id\n }\n }\n }\n }\n ": types.Test_CreateGroupDocument, - "\n mutation Test_CreateModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n }\n }\n }\n }\n ": types.Test_CreateModelDocument, - "\n mutation Test_Invite($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n }\n }\n }\n }\n ": types.Test_InviteDocument, - "\n mutation Test_AcceptInvite($input: MutationReactToGroupInviteInput!) {\n result: reactToGroupInvite(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on ReactToGroupInviteResult {\n __typename\n }\n }\n }\n ": types.Test_AcceptInviteDocument, - "\n mutation CreateGroupTest {\n result: createGroup(input: { slug: \"testgroup\" }) {\n __typename\n ... on CreateGroupResult {\n group {\n id\n slug\n memberships {\n edges {\n node {\n user {\n username\n }\n role\n }\n }\n }\n }\n }\n ... on BaseError {\n message\n }\n }\n }\n": types.CreateGroupTestDocument, - "\n mutation CreateSquiggleSnippetModelTest(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n issues {\n message\n }\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n slug\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n": types.CreateSquiggleSnippetModelTestDocument, - "\n mutation DeleteMembershipTest($group: String!, $user: String!) {\n result: deleteMembership(input: { group: $group, user: $user }) {\n __typename\n ... on BaseError {\n message\n }\n ... on DeleteMembershipResult {\n ok\n }\n }\n }\n ": types.DeleteMembershipTestDocument, - "\n mutation DeleteModelTest($input: MutationDeleteModelInput!) {\n result: deleteModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on NotFoundError {\n message\n }\n ... on DeleteModelResult {\n ok\n }\n }\n }\n": types.DeleteModelTestDocument, - "\n mutation InviteTest($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n role\n }\n }\n }\n }\n ": types.InviteTestDocument, - "\n mutation SetUsernameTest($username: String!) {\n result: setUsername(username: $username) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n message\n }\n ... on Me {\n email\n username\n }\n }\n }\n": types.SetUsernameTestDocument, - "\n query TestGroups($input: GroupsQueryInput!) {\n result: groups(first: 10, input: $input) {\n __typename\n edges {\n node {\n id\n slug\n }\n }\n }\n }\n ": types.TestGroupsDocument, - "\n query TestMe {\n me {\n __typename\n email\n username\n }\n }\n ": types.TestMeDocument, - "\n query TestModels {\n models {\n edges {\n node {\n slug\n isEditable\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n": types.TestModelsDocument, - "\n mutation TestModels_createModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n }\n }\n": types.TestModels_CreateModelDocument, - "\n query TestUserByUsername($username: String!) {\n result: userByUsername(username: $username) {\n __typename\n ... on User {\n slug\n username\n }\n ... on Error {\n message\n }\n }\n }\n": types.TestUserByUsernameDocument, - "\n query TestUsers($input: UsersQueryInput) {\n result: users(input: $input) {\n edges {\n node {\n username\n }\n }\n }\n }\n": types.TestUsersDocument, -}; - -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - * - * - * @example - * ```ts - * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); - * ``` - * - * The query argument is unknown! - * Please regenerate the types. - */ -export function graphql(source: string): unknown; - -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query Test_MyMembership($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n myMembership {\n id\n role\n }\n }\n }\n }\n "): (typeof documents)["\n query Test_MyMembership($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n myMembership {\n id\n role\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query Test_Memberships($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n memberships {\n edges {\n node {\n id\n role\n user {\n slug\n }\n }\n }\n }\n }\n }\n }\n "): (typeof documents)["\n query Test_Memberships($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n memberships {\n edges {\n node {\n id\n role\n user {\n slug\n }\n }\n }\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation Test_CreateGroup($input: MutationCreateGroupInput!) {\n result: createGroup(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateGroupResult {\n group {\n id\n }\n }\n }\n }\n "): (typeof documents)["\n mutation Test_CreateGroup($input: MutationCreateGroupInput!) {\n result: createGroup(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateGroupResult {\n group {\n id\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation Test_CreateModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n }\n }\n }\n }\n "): (typeof documents)["\n mutation Test_CreateModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation Test_Invite($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n }\n }\n }\n }\n "): (typeof documents)["\n mutation Test_Invite($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation Test_AcceptInvite($input: MutationReactToGroupInviteInput!) {\n result: reactToGroupInvite(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on ReactToGroupInviteResult {\n __typename\n }\n }\n }\n "): (typeof documents)["\n mutation Test_AcceptInvite($input: MutationReactToGroupInviteInput!) {\n result: reactToGroupInvite(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on ReactToGroupInviteResult {\n __typename\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation CreateGroupTest {\n result: createGroup(input: { slug: \"testgroup\" }) {\n __typename\n ... on CreateGroupResult {\n group {\n id\n slug\n memberships {\n edges {\n node {\n user {\n username\n }\n role\n }\n }\n }\n }\n }\n ... on BaseError {\n message\n }\n }\n }\n"): (typeof documents)["\n mutation CreateGroupTest {\n result: createGroup(input: { slug: \"testgroup\" }) {\n __typename\n ... on CreateGroupResult {\n group {\n id\n slug\n memberships {\n edges {\n node {\n user {\n username\n }\n role\n }\n }\n }\n }\n }\n ... on BaseError {\n message\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation CreateSquiggleSnippetModelTest(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n issues {\n message\n }\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n slug\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateSquiggleSnippetModelTest(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n issues {\n message\n }\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n slug\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation DeleteMembershipTest($group: String!, $user: String!) {\n result: deleteMembership(input: { group: $group, user: $user }) {\n __typename\n ... on BaseError {\n message\n }\n ... on DeleteMembershipResult {\n ok\n }\n }\n }\n "): (typeof documents)["\n mutation DeleteMembershipTest($group: String!, $user: String!) {\n result: deleteMembership(input: { group: $group, user: $user }) {\n __typename\n ... on BaseError {\n message\n }\n ... on DeleteMembershipResult {\n ok\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation DeleteModelTest($input: MutationDeleteModelInput!) {\n result: deleteModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on NotFoundError {\n message\n }\n ... on DeleteModelResult {\n ok\n }\n }\n }\n"): (typeof documents)["\n mutation DeleteModelTest($input: MutationDeleteModelInput!) {\n result: deleteModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on NotFoundError {\n message\n }\n ... on DeleteModelResult {\n ok\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation InviteTest($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n role\n }\n }\n }\n }\n "): (typeof documents)["\n mutation InviteTest($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n role\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation SetUsernameTest($username: String!) {\n result: setUsername(username: $username) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n message\n }\n ... on Me {\n email\n username\n }\n }\n }\n"): (typeof documents)["\n mutation SetUsernameTest($username: String!) {\n result: setUsername(username: $username) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n message\n }\n ... on Me {\n email\n username\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestGroups($input: GroupsQueryInput!) {\n result: groups(first: 10, input: $input) {\n __typename\n edges {\n node {\n id\n slug\n }\n }\n }\n }\n "): (typeof documents)["\n query TestGroups($input: GroupsQueryInput!) {\n result: groups(first: 10, input: $input) {\n __typename\n edges {\n node {\n id\n slug\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestMe {\n me {\n __typename\n email\n username\n }\n }\n "): (typeof documents)["\n query TestMe {\n me {\n __typename\n email\n username\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestModels {\n models {\n edges {\n node {\n slug\n isEditable\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query TestModels {\n models {\n edges {\n node {\n slug\n isEditable\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation TestModels_createModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n }\n }\n"): (typeof documents)["\n mutation TestModels_createModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestUserByUsername($username: String!) {\n result: userByUsername(username: $username) {\n __typename\n ... on User {\n slug\n username\n }\n ... on Error {\n message\n }\n }\n }\n"): (typeof documents)["\n query TestUserByUsername($username: String!) {\n result: userByUsername(username: $username) {\n __typename\n ... on User {\n slug\n username\n }\n ... on Error {\n message\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestUsers($input: UsersQueryInput) {\n result: users(input: $input) {\n edges {\n node {\n username\n }\n }\n }\n }\n"): (typeof documents)["\n query TestUsers($input: UsersQueryInput) {\n result: users(input: $input) {\n edges {\n node {\n username\n }\n }\n }\n }\n"]; - -export function graphql(source: string) { - return (documents as any)[source] ?? {}; -} - -export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; \ No newline at end of file diff --git a/packages/hub/test/gql-gen/graphql.ts b/packages/hub/test/gql-gen/graphql.ts deleted file mode 100644 index 135050c3b3..0000000000 --- a/packages/hub/test/gql-gen/graphql.ts +++ /dev/null @@ -1,1157 +0,0 @@ -/* eslint-disable */ -import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core"; - -export type Maybe = T | null; -export type InputMaybe = Maybe; -export type Exact = { [K in keyof T]: T[K] }; -export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -export type MakeEmpty = { [_ in K]?: never }; -export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: { input: string; output: string; } - String: { input: string; output: string; } - Boolean: { input: boolean; output: boolean; } - Int: { input: number; output: number; } - Float: { input: number; output: number; } -}; - -export type AcceptReusableGroupInviteTokenResult = { - __typename?: 'AcceptReusableGroupInviteTokenResult'; - membership: UserGroupMembership; -}; - -export type AdminUpdateModelVersionResult = { - __typename?: 'AdminUpdateModelVersionResult'; - model: Model; -}; - -export type BaseError = Error & { - __typename?: 'BaseError'; - message: Scalars['String']['output']; -}; - -export type BuildRelativeValuesCacheResult = { - __typename?: 'BuildRelativeValuesCacheResult'; - relativeValuesExport: RelativeValuesExport; -}; - -export type CancelGroupInviteResult = { - __typename?: 'CancelGroupInviteResult'; - invite: GroupInvite; -}; - -export type ClearRelativeValuesCacheResult = { - __typename?: 'ClearRelativeValuesCacheResult'; - relativeValuesExport: RelativeValuesExport; -}; - -export type CreateGroupResult = { - __typename?: 'CreateGroupResult'; - group: Group; -}; - -export type CreateRelativeValuesDefinitionResult = { - __typename?: 'CreateRelativeValuesDefinitionResult'; - definition: RelativeValuesDefinition; -}; - -export type CreateReusableGroupInviteTokenResult = { - __typename?: 'CreateReusableGroupInviteTokenResult'; - group: Group; -}; - -export type CreateSquiggleSnippetModelResult = { - __typename?: 'CreateSquiggleSnippetModelResult'; - model: Model; -}; - -export type DefinitionRefInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type DeleteMembershipResult = { - __typename?: 'DeleteMembershipResult'; - ok: Scalars['Boolean']['output']; -}; - -export type DeleteModelResult = { - __typename?: 'DeleteModelResult'; - ok: Scalars['Boolean']['output']; -}; - -export type DeleteRelativeValuesDefinitionResult = { - __typename?: 'DeleteRelativeValuesDefinitionResult'; - ok: Scalars['Boolean']['output']; -}; - -export type DeleteReusableGroupInviteTokenResult = { - __typename?: 'DeleteReusableGroupInviteTokenResult'; - group: Group; -}; - -export type EmailGroupInvite = GroupInvite & Node & { - __typename?: 'EmailGroupInvite'; - email: Scalars['String']['output']; - group: Group; - id: Scalars['ID']['output']; - role: MembershipRole; -}; - -export type Error = { - message: Scalars['String']['output']; -}; - -export type GlobalStatistics = { - __typename?: 'GlobalStatistics'; - models: Scalars['Int']['output']; - relativeValuesDefinitions: Scalars['Int']['output']; - users: Scalars['Int']['output']; -}; - -export type Group = Node & Owner & { - __typename?: 'Group'; - createdAtTimestamp: Scalars['Float']['output']; - id: Scalars['ID']['output']; - inviteForMe?: Maybe; - invites?: Maybe; - memberships: UserGroupMembershipConnection; - models: ModelConnection; - myMembership?: Maybe; - reusableInviteToken?: Maybe; - slug: Scalars['String']['output']; - updatedAtTimestamp: Scalars['Float']['output']; -}; - - -export type GroupInvitesArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type GroupMembershipsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type GroupModelsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type GroupConnection = { - __typename?: 'GroupConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type GroupEdge = { - __typename?: 'GroupEdge'; - cursor: Scalars['String']['output']; - node: Group; -}; - -export type GroupInvite = { - group: Group; - id: Scalars['ID']['output']; - role: MembershipRole; -}; - -export type GroupInviteConnection = { - __typename?: 'GroupInviteConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type GroupInviteEdge = { - __typename?: 'GroupInviteEdge'; - cursor: Scalars['String']['output']; - node: GroupInvite; -}; - -export enum GroupInviteReaction { - Accept = 'Accept', - Decline = 'Decline' -} - -export type GroupsQueryInput = { - /** List only groups that you're a member of */ - myOnly?: InputMaybe; - slugContains?: InputMaybe; -}; - -export type InviteUserToGroupResult = { - __typename?: 'InviteUserToGroupResult'; - invite: GroupInvite; -}; - -export type Me = { - __typename?: 'Me'; - asUser: User; - email: Scalars['String']['output']; - username?: Maybe; -}; - -export enum MembershipRole { - Admin = 'Admin', - Member = 'Member' -} - -export type Model = Node & { - __typename?: 'Model'; - createdAtTimestamp: Scalars['Float']['output']; - currentRevision: ModelRevision; - id: Scalars['ID']['output']; - isEditable: Scalars['Boolean']['output']; - isPrivate: Scalars['Boolean']['output']; - owner: Owner; - revision: ModelRevision; - revisions: ModelRevisionConnection; - slug: Scalars['String']['output']; - updatedAtTimestamp: Scalars['Float']['output']; -}; - - -export type ModelRevisionArgs = { - id: Scalars['ID']['input']; -}; - - -export type ModelRevisionsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type ModelConnection = { - __typename?: 'ModelConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type ModelContent = SquiggleSnippet; - -export type ModelEdge = { - __typename?: 'ModelEdge'; - cursor: Scalars['String']['output']; - node: Model; -}; - -export type Variable = Node & { - __typename?: 'Variable'; - docstring: Scalars['String']['output']; - id: Scalars['ID']['output']; - modelRevision: ModelRevision; - title?: Maybe; - variableName: Scalars['String']['output']; - variableType: Scalars['String']['output']; -}; - -export type ModelRevision = Node & { - __typename?: 'ModelRevision'; - author?: Maybe; - comment: Scalars['String']['output']; - content: ModelContent; - createdAtTimestamp: Scalars['Float']['output']; - variables: Array; - forRelativeValues?: Maybe; - id: Scalars['ID']['output']; - model: Model; - relativeValuesExports: Array; -}; - - -export type ModelRevisionForRelativeValuesArgs = { - input?: InputMaybe; -}; - -export type ModelRevisionConnection = { - __typename?: 'ModelRevisionConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type ModelRevisionEdge = { - __typename?: 'ModelRevisionEdge'; - cursor: Scalars['String']['output']; - node: ModelRevision; -}; - -export type ModelRevisionForRelativeValuesInput = { - for?: InputMaybe; - variableName: Scalars['String']['input']; -}; - -export type ModelRevisionForRelativeValuesSlugOwnerInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type ModelsByVersion = { - __typename?: 'ModelsByVersion'; - count: Scalars['Int']['output']; - models: Array; - privateCount: Scalars['Int']['output']; - version: Scalars['String']['output']; -}; - -export type MoveModelResult = { - __typename?: 'MoveModelResult'; - model: Model; -}; - -export type Mutation = { - __typename?: 'Mutation'; - acceptReusableGroupInviteToken: MutationAcceptReusableGroupInviteTokenResult; - /** Admin-only query for upgrading model versions */ - adminUpdateModelVersion: MutationAdminUpdateModelVersionResult; - buildRelativeValuesCache: MutationBuildRelativeValuesCacheResult; - cancelGroupInvite: MutationCancelGroupInviteResult; - clearRelativeValuesCache: MutationClearRelativeValuesCacheResult; - createGroup: MutationCreateGroupResult; - createRelativeValuesDefinition: MutationCreateRelativeValuesDefinitionResult; - /** - * Create or replace a reusable invite token for a group, available as `reusableInviteToken` field on group object. - * - * You must be an admin of the group to call this mutation. Previous invite token, if it existed, will stop working. - */ - createReusableGroupInviteToken: MutationCreateReusableGroupInviteTokenResult; - createSquiggleSnippetModel: MutationCreateSquiggleSnippetModelResult; - deleteMembership: MutationDeleteMembershipResult; - deleteModel: MutationDeleteModelResult; - deleteRelativeValuesDefinition: MutationDeleteRelativeValuesDefinitionResult; - /** Disable a reusable invite token for a group. */ - deleteReusableGroupInviteToken: MutationDeleteReusableGroupInviteTokenResult; - inviteUserToGroup: MutationInviteUserToGroupResult; - moveModel: MutationMoveModelResult; - reactToGroupInvite: MutationReactToGroupInviteResult; - setUsername: MutationSetUsernameResult; - updateGroupInviteRole: MutationUpdateGroupInviteRoleResult; - updateMembershipRole: MutationUpdateMembershipRoleResult; - updateModelPrivacy: MutationUpdateModelPrivacyResult; - updateModelSlug: MutationUpdateModelSlugResult; - updateRelativeValuesDefinition: MutationUpdateRelativeValuesDefinitionResult; - updateSquiggleSnippetModel: MutationUpdateSquiggleSnippetModelResult; -}; - - -export type MutationAcceptReusableGroupInviteTokenArgs = { - input: MutationAcceptReusableGroupInviteTokenInput; -}; - - -export type MutationAdminUpdateModelVersionArgs = { - input: MutationAdminUpdateModelVersionInput; -}; - - -export type MutationBuildRelativeValuesCacheArgs = { - input: MutationBuildRelativeValuesCacheInput; -}; - - -export type MutationCancelGroupInviteArgs = { - input: MutationCancelGroupInviteInput; -}; - - -export type MutationClearRelativeValuesCacheArgs = { - input: MutationClearRelativeValuesCacheInput; -}; - - -export type MutationCreateGroupArgs = { - input: MutationCreateGroupInput; -}; - - -export type MutationCreateRelativeValuesDefinitionArgs = { - input: MutationCreateRelativeValuesDefinitionInput; -}; - - -export type MutationCreateReusableGroupInviteTokenArgs = { - input: MutationCreateReusableGroupInviteTokenInput; -}; - - -export type MutationCreateSquiggleSnippetModelArgs = { - input: MutationCreateSquiggleSnippetModelInput; -}; - - -export type MutationDeleteMembershipArgs = { - input: MutationDeleteMembershipInput; -}; - - -export type MutationDeleteModelArgs = { - input: MutationDeleteModelInput; -}; - - -export type MutationDeleteRelativeValuesDefinitionArgs = { - input: MutationDeleteRelativeValuesDefinitionInput; -}; - - -export type MutationDeleteReusableGroupInviteTokenArgs = { - input: MutationDeleteReusableGroupInviteTokenInput; -}; - - -export type MutationInviteUserToGroupArgs = { - input: MutationInviteUserToGroupInput; -}; - - -export type MutationMoveModelArgs = { - input: MutationMoveModelInput; -}; - - -export type MutationReactToGroupInviteArgs = { - input: MutationReactToGroupInviteInput; -}; - - -export type MutationSetUsernameArgs = { - username: Scalars['String']['input']; -}; - - -export type MutationUpdateGroupInviteRoleArgs = { - input: MutationUpdateGroupInviteRoleInput; -}; - - -export type MutationUpdateMembershipRoleArgs = { - input: MutationUpdateMembershipRoleInput; -}; - - -export type MutationUpdateModelPrivacyArgs = { - input: MutationUpdateModelPrivacyInput; -}; - - -export type MutationUpdateModelSlugArgs = { - input: MutationUpdateModelSlugInput; -}; - - -export type MutationUpdateRelativeValuesDefinitionArgs = { - input: MutationUpdateRelativeValuesDefinitionInput; -}; - - -export type MutationUpdateSquiggleSnippetModelArgs = { - input: MutationUpdateSquiggleSnippetModelInput; -}; - -export type MutationAcceptReusableGroupInviteTokenInput = { - groupSlug: Scalars['String']['input']; - inviteToken: Scalars['String']['input']; -}; - -export type MutationAcceptReusableGroupInviteTokenResult = AcceptReusableGroupInviteTokenResult | BaseError; - -export type MutationAdminUpdateModelVersionInput = { - modelId: Scalars['String']['input']; - version: Scalars['String']['input']; -}; - -export type MutationAdminUpdateModelVersionResult = AdminUpdateModelVersionResult | BaseError; - -export type MutationBuildRelativeValuesCacheInput = { - exportId: Scalars['String']['input']; -}; - -export type MutationBuildRelativeValuesCacheResult = BaseError | BuildRelativeValuesCacheResult; - -export type MutationCancelGroupInviteInput = { - inviteId: Scalars['String']['input']; -}; - -export type MutationCancelGroupInviteResult = BaseError | CancelGroupInviteResult; - -export type MutationClearRelativeValuesCacheInput = { - exportId: Scalars['String']['input']; -}; - -export type MutationClearRelativeValuesCacheResult = BaseError | ClearRelativeValuesCacheResult; - -export type MutationCreateGroupInput = { - slug: Scalars['String']['input']; -}; - -export type MutationCreateGroupResult = BaseError | CreateGroupResult; - -export type MutationCreateRelativeValuesDefinitionInput = { - clusters: Array; - /** Optional, if not set, definition will be created on current user's account */ - groupSlug?: InputMaybe; - items: Array; - recommendedUnit?: InputMaybe; - slug: Scalars['String']['input']; - title: Scalars['String']['input']; -}; - -export type MutationCreateRelativeValuesDefinitionResult = BaseError | CreateRelativeValuesDefinitionResult | ValidationError; - -export type MutationCreateReusableGroupInviteTokenInput = { - slug: Scalars['String']['input']; -}; - -export type MutationCreateReusableGroupInviteTokenResult = BaseError | CreateReusableGroupInviteTokenResult; - -export type MutationCreateSquiggleSnippetModelInput = { - /** Squiggle source code */ - code: Scalars['String']['input']; - /** Optional, if not set, model will be created on current user's account */ - groupSlug?: InputMaybe; - /** Defaults to false */ - isPrivate?: InputMaybe; - slug: Scalars['String']['input']; - version: Scalars['String']['input']; -}; - -export type MutationCreateSquiggleSnippetModelResult = BaseError | CreateSquiggleSnippetModelResult | ValidationError; - -export type MutationDeleteMembershipInput = { - group: Scalars['String']['input']; - user: Scalars['String']['input']; -}; - -export type MutationDeleteMembershipResult = BaseError | DeleteMembershipResult; - -export type MutationDeleteModelInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type MutationDeleteModelResult = BaseError | DeleteModelResult | NotFoundError | ValidationError; - -export type MutationDeleteRelativeValuesDefinitionInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type MutationDeleteRelativeValuesDefinitionResult = BaseError | DeleteRelativeValuesDefinitionResult; - -export type MutationDeleteReusableGroupInviteTokenInput = { - slug: Scalars['String']['input']; -}; - -export type MutationDeleteReusableGroupInviteTokenResult = BaseError | DeleteReusableGroupInviteTokenResult; - -export type MutationInviteUserToGroupInput = { - group: Scalars['String']['input']; - role: MembershipRole; - username: Scalars['String']['input']; -}; - -export type MutationInviteUserToGroupResult = BaseError | InviteUserToGroupResult | ValidationError; - -export type MutationMoveModelInput = { - newOwner: Scalars['String']['input']; - oldOwner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type MutationMoveModelResult = BaseError | MoveModelResult | NotFoundError | ValidationError; - -export type MutationReactToGroupInviteInput = { - action: GroupInviteReaction; - inviteId: Scalars['String']['input']; -}; - -export type MutationReactToGroupInviteResult = BaseError | ReactToGroupInviteResult; - -export type MutationSetUsernameResult = BaseError | Me | ValidationError; - -export type MutationUpdateGroupInviteRoleInput = { - inviteId: Scalars['String']['input']; - role: MembershipRole; -}; - -export type MutationUpdateGroupInviteRoleResult = BaseError | UpdateGroupInviteRoleResult; - -export type MutationUpdateMembershipRoleInput = { - group: Scalars['String']['input']; - role: MembershipRole; - user: Scalars['String']['input']; -}; - -export type MutationUpdateMembershipRoleResult = BaseError | UpdateMembershipRoleResult; - -export type MutationUpdateModelPrivacyInput = { - isPrivate: Scalars['Boolean']['input']; - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type MutationUpdateModelPrivacyResult = BaseError | UpdateModelPrivacyResult; - -export type MutationUpdateModelSlugInput = { - newSlug: Scalars['String']['input']; - oldSlug: Scalars['String']['input']; - owner: Scalars['String']['input']; -}; - -export type MutationUpdateModelSlugResult = BaseError | UpdateModelSlugResult; - -export type MutationUpdateRelativeValuesDefinitionInput = { - clusters: Array; - items: Array; - owner: Scalars['String']['input']; - recommendedUnit?: InputMaybe; - slug: Scalars['String']['input']; - title: Scalars['String']['input']; -}; - -export type MutationUpdateRelativeValuesDefinitionResult = BaseError | UpdateRelativeValuesDefinitionResult; - -export type MutationUpdateSquiggleSnippetModelInput = { - comment?: InputMaybe; - content: SquiggleSnippetContentInput; - variables?: InputMaybe>; - owner: Scalars['String']['input']; - relativeValuesExports?: InputMaybe>; - slug: Scalars['String']['input']; -}; - -export type MutationUpdateSquiggleSnippetModelResult = BaseError | UpdateSquiggleSnippetResult; - -export type Node = { - id: Scalars['ID']['output']; -}; - -export type NotFoundError = Error & { - __typename?: 'NotFoundError'; - message: Scalars['String']['output']; -}; - -export type Owner = { - id: Scalars['ID']['output']; - slug: Scalars['String']['output']; -}; - -export type PageInfo = { - __typename?: 'PageInfo'; - endCursor?: Maybe; - hasNextPage: Scalars['Boolean']['output']; - hasPreviousPage: Scalars['Boolean']['output']; - startCursor?: Maybe; -}; - -export type Query = { - __typename?: 'Query'; - globalStatistics: GlobalStatistics; - group: QueryGroupResult; - groups: GroupConnection; - me: Me; - model: QueryModelResult; - models: ModelConnection; - /** Admin-only query for listing models in /admin UI */ - modelsByVersion: Array; - node?: Maybe; - nodes: Array>; - relativeValuesDefinition: QueryRelativeValuesDefinitionResult; - relativeValuesDefinitions: RelativeValuesDefinitionConnection; - runSquiggle: SquiggleOutput; - userByUsername: QueryUserByUsernameResult; - users: QueryUsersConnection; -}; - - -export type QueryGroupArgs = { - slug: Scalars['String']['input']; -}; - - -export type QueryGroupsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - input?: InputMaybe; - last?: InputMaybe; -}; - - -export type QueryModelArgs = { - input: QueryModelInput; -}; - - -export type QueryModelsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type QueryNodeArgs = { - id: Scalars['ID']['input']; -}; - - -export type QueryNodesArgs = { - ids: Array; -}; - - -export type QueryRelativeValuesDefinitionArgs = { - input: QueryRelativeValuesDefinitionInput; -}; - - -export type QueryRelativeValuesDefinitionsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - input?: InputMaybe; - last?: InputMaybe; -}; - - -export type QueryRunSquiggleArgs = { - code: Scalars['String']['input']; -}; - - -export type QueryUserByUsernameArgs = { - username: Scalars['String']['input']; -}; - - -export type QueryUsersArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - input?: InputMaybe; - last?: InputMaybe; -}; - -export type QueryGroupResult = BaseError | Group | NotFoundError; - -export type QueryModelInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type QueryModelResult = BaseError | Model | NotFoundError; - -export type QueryRelativeValuesDefinitionInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type QueryRelativeValuesDefinitionResult = BaseError | NotFoundError | RelativeValuesDefinition; - -export type QueryUserByUsernameResult = BaseError | NotFoundError | User; - -export type QueryUsersConnection = { - __typename?: 'QueryUsersConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type QueryUsersConnectionEdge = { - __typename?: 'QueryUsersConnectionEdge'; - cursor: Scalars['String']['output']; - node: User; -}; - -export type ReactToGroupInviteResult = { - __typename?: 'ReactToGroupInviteResult'; - invite: GroupInvite; - membership?: Maybe; -}; - -export type RelativeValuesCluster = { - __typename?: 'RelativeValuesCluster'; - color: Scalars['String']['output']; - id: Scalars['String']['output']; - recommendedUnit?: Maybe; -}; - -export type RelativeValuesClusterInput = { - color: Scalars['String']['input']; - id: Scalars['String']['input']; - recommendedUnit?: InputMaybe; -}; - -export type RelativeValuesDefinition = Node & { - __typename?: 'RelativeValuesDefinition'; - createdAtTimestamp: Scalars['Float']['output']; - currentRevision: RelativeValuesDefinitionRevision; - id: Scalars['ID']['output']; - isEditable: Scalars['Boolean']['output']; - Variables: Array; - owner: Owner; - slug: Scalars['String']['output']; - updatedAtTimestamp: Scalars['Float']['output']; -}; - -export type RelativeValuesDefinitionConnection = { - __typename?: 'RelativeValuesDefinitionConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type RelativeValuesDefinitionEdge = { - __typename?: 'RelativeValuesDefinitionEdge'; - cursor: Scalars['String']['output']; - node: RelativeValuesDefinition; -}; - -export type RelativeValuesDefinitionRevision = Node & { - __typename?: 'RelativeValuesDefinitionRevision'; - clusters: Array; - id: Scalars['ID']['output']; - items: Array; - recommendedUnit?: Maybe; - title: Scalars['String']['output']; -}; - -export type RelativeValuesDefinitionsQueryInput = { - owner?: InputMaybe; - slugContains?: InputMaybe; -}; - -export type RelativeValuesExport = Node & { - __typename?: 'RelativeValuesExport'; - cache: Array; - definition: RelativeValuesDefinition; - id: Scalars['ID']['output']; - modelRevision: ModelRevision; - variableName: Scalars['String']['output']; -}; - -export type RelativeValuesExportInput = { - definition: DefinitionRefInput; - variableName: Scalars['String']['input']; -}; - -export type RelativeValuesItem = { - __typename?: 'RelativeValuesItem'; - clusterId?: Maybe; - description: Scalars['String']['output']; - id: Scalars['String']['output']; - name: Scalars['String']['output']; -}; - -export type RelativeValuesItemInput = { - clusterId?: InputMaybe; - description?: InputMaybe; - id: Scalars['String']['input']; - name: Scalars['String']['input']; -}; - -export type RelativeValuesPairCache = Node & { - __typename?: 'RelativeValuesPairCache'; - errorString?: Maybe; - firstItem: Scalars['String']['output']; - id: Scalars['ID']['output']; - resultJSON: Scalars['String']['output']; - secondItem: Scalars['String']['output']; -}; - -export type SquiggleErrorOutput = SquiggleOutput & { - __typename?: 'SquiggleErrorOutput'; - errorString: Scalars['String']['output']; - isCached: Scalars['Boolean']['output']; -}; - -export type SquiggleVariableInput = { - docstring?: InputMaybe; - title?: InputMaybe; - variableName: Scalars['String']['input']; - variableType: Scalars['String']['input']; -}; - -export type SquiggleOkOutput = SquiggleOutput & { - __typename?: 'SquiggleOkOutput'; - bindingsJSON: Scalars['String']['output']; - isCached: Scalars['Boolean']['output']; - resultJSON: Scalars['String']['output']; -}; - -export type SquiggleOutput = { - isCached: Scalars['Boolean']['output']; -}; - -export type SquiggleSnippet = Node & { - __typename?: 'SquiggleSnippet'; - code: Scalars['String']['output']; - id: Scalars['ID']['output']; - version: Scalars['String']['output']; -}; - -export type SquiggleSnippetContentInput = { - code: Scalars['String']['input']; - version: Scalars['String']['input']; -}; - -export type UpdateGroupInviteRoleResult = { - __typename?: 'UpdateGroupInviteRoleResult'; - invite: GroupInvite; -}; - -export type UpdateMembershipRoleResult = { - __typename?: 'UpdateMembershipRoleResult'; - membership: UserGroupMembership; -}; - -export type UpdateModelPrivacyResult = { - __typename?: 'UpdateModelPrivacyResult'; - model: Model; -}; - -export type UpdateModelSlugResult = { - __typename?: 'UpdateModelSlugResult'; - model: Model; -}; - -export type UpdateRelativeValuesDefinitionResult = { - __typename?: 'UpdateRelativeValuesDefinitionResult'; - definition: RelativeValuesDefinition; -}; - -export type UpdateSquiggleSnippetResult = { - __typename?: 'UpdateSquiggleSnippetResult'; - model: Model; -}; - -export type User = Node & Owner & { - __typename?: 'User'; - groups: GroupConnection; - id: Scalars['ID']['output']; - isRoot: Scalars['Boolean']['output']; - models: ModelConnection; - relativeValuesDefinitions: RelativeValuesDefinitionConnection; - slug: Scalars['String']['output']; - username: Scalars['String']['output']; -}; - - -export type UserGroupsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type UserModelsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type UserRelativeValuesDefinitionsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type UserGroupInvite = GroupInvite & Node & { - __typename?: 'UserGroupInvite'; - group: Group; - id: Scalars['ID']['output']; - role: MembershipRole; - user: User; -}; - -export type UserGroupMembership = Node & { - __typename?: 'UserGroupMembership'; - group: Group; - id: Scalars['ID']['output']; - role: MembershipRole; - user: User; -}; - -export type UserGroupMembershipConnection = { - __typename?: 'UserGroupMembershipConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type UserGroupMembershipEdge = { - __typename?: 'UserGroupMembershipEdge'; - cursor: Scalars['String']['output']; - node: UserGroupMembership; -}; - -export type UsersQueryInput = { - usernameContains?: InputMaybe; -}; - -export type ValidationError = Error & { - __typename?: 'ValidationError'; - issues: Array; - message: Scalars['String']['output']; -}; - -export type ValidationErrorIssue = { - __typename?: 'ValidationErrorIssue'; - message: Scalars['String']['output']; - path: Array; -}; - -export type Test_MyMembershipQueryVariables = Exact<{ - slug: Scalars['String']['input']; -}>; - - -export type Test_MyMembershipQuery = { __typename?: 'Query', result: { __typename: 'BaseError' } | { __typename: 'Group', id: string, myMembership?: { __typename?: 'UserGroupMembership', id: string, role: MembershipRole } | null } | { __typename: 'NotFoundError' } }; - -export type Test_MembershipsQueryVariables = Exact<{ - slug: Scalars['String']['input']; -}>; - - -export type Test_MembershipsQuery = { __typename?: 'Query', result: { __typename: 'BaseError' } | { __typename: 'Group', id: string, memberships: { __typename?: 'UserGroupMembershipConnection', edges: Array<{ __typename?: 'UserGroupMembershipEdge', node: { __typename?: 'UserGroupMembership', id: string, role: MembershipRole, user: { __typename?: 'User', slug: string } } }> } } | { __typename: 'NotFoundError' } }; - -export type Test_CreateGroupMutationVariables = Exact<{ - input: MutationCreateGroupInput; -}>; - - -export type Test_CreateGroupMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'CreateGroupResult', group: { __typename?: 'Group', id: string } } }; - -export type Test_CreateModelMutationVariables = Exact<{ - input: MutationCreateSquiggleSnippetModelInput; -}>; - - -export type Test_CreateModelMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'CreateSquiggleSnippetModelResult', model: { __typename?: 'Model', id: string } } | { __typename: 'ValidationError', message: string } }; - -export type Test_InviteMutationVariables = Exact<{ - input: MutationInviteUserToGroupInput; -}>; - - -export type Test_InviteMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'InviteUserToGroupResult', invite: { __typename?: 'EmailGroupInvite', id: string } | { __typename?: 'UserGroupInvite', id: string } } | { __typename: 'ValidationError' } }; - -export type Test_AcceptInviteMutationVariables = Exact<{ - input: MutationReactToGroupInviteInput; -}>; - - -export type Test_AcceptInviteMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'ReactToGroupInviteResult' } }; - -export type CreateGroupTestMutationVariables = Exact<{ [key: string]: never; }>; - - -export type CreateGroupTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'CreateGroupResult', group: { __typename?: 'Group', id: string, slug: string, memberships: { __typename?: 'UserGroupMembershipConnection', edges: Array<{ __typename?: 'UserGroupMembershipEdge', node: { __typename?: 'UserGroupMembership', role: MembershipRole, user: { __typename?: 'User', username: string } } }> } } } }; - -export type CreateSquiggleSnippetModelTestMutationVariables = Exact<{ - input: MutationCreateSquiggleSnippetModelInput; -}>; - - -export type CreateSquiggleSnippetModelTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'CreateSquiggleSnippetModelResult', model: { __typename?: 'Model', id: string, slug: string, isPrivate: boolean, owner: { __typename: 'Group', slug: string } | { __typename: 'User', slug: string } } } | { __typename: 'ValidationError', message: string, issues: Array<{ __typename?: 'ValidationErrorIssue', message: string }> } }; - -export type DeleteMembershipTestMutationVariables = Exact<{ - group: Scalars['String']['input']; - user: Scalars['String']['input']; -}>; - - -export type DeleteMembershipTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'DeleteMembershipResult', ok: boolean } }; - -export type DeleteModelTestMutationVariables = Exact<{ - input: MutationDeleteModelInput; -}>; - - -export type DeleteModelTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'DeleteModelResult', ok: boolean } | { __typename: 'NotFoundError', message: string } | { __typename: 'ValidationError', message: string } }; - -export type InviteTestMutationVariables = Exact<{ - input: MutationInviteUserToGroupInput; -}>; - - -export type InviteTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'InviteUserToGroupResult', invite: { __typename?: 'EmailGroupInvite', id: string, role: MembershipRole } | { __typename?: 'UserGroupInvite', id: string, role: MembershipRole } } | { __typename: 'ValidationError' } }; - -export type SetUsernameTestMutationVariables = Exact<{ - username: Scalars['String']['input']; -}>; - - -export type SetUsernameTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'Me', email: string, username?: string | null } | { __typename: 'ValidationError', message: string } }; - -export type TestGroupsQueryVariables = Exact<{ - input: GroupsQueryInput; -}>; - - -export type TestGroupsQuery = { __typename?: 'Query', result: { __typename: 'GroupConnection', edges: Array<{ __typename?: 'GroupEdge', node: { __typename?: 'Group', id: string, slug: string } }> } }; - -export type TestMeQueryVariables = Exact<{ [key: string]: never; }>; - - -export type TestMeQuery = { __typename?: 'Query', me: { __typename: 'Me', email: string, username?: string | null } }; - -export type TestModelsQueryVariables = Exact<{ [key: string]: never; }>; - - -export type TestModelsQuery = { __typename?: 'Query', models: { __typename?: 'ModelConnection', edges: Array<{ __typename?: 'ModelEdge', node: { __typename?: 'Model', slug: string, isEditable: boolean, isPrivate: boolean, owner: { __typename: 'Group', slug: string } | { __typename: 'User', slug: string } } }> } }; - -export type TestModels_CreateModelMutationVariables = Exact<{ - input: MutationCreateSquiggleSnippetModelInput; -}>; - - -export type TestModels_CreateModelMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError' } | { __typename: 'CreateSquiggleSnippetModelResult' } | { __typename: 'ValidationError' } }; - -export type TestUserByUsernameQueryVariables = Exact<{ - username: Scalars['String']['input']; -}>; - - -export type TestUserByUsernameQuery = { __typename?: 'Query', result: { __typename: 'BaseError', message: string } | { __typename: 'NotFoundError', message: string } | { __typename: 'User', slug: string, username: string } }; - -export type TestUsersQueryVariables = Exact<{ - input?: InputMaybe; -}>; - - -export type TestUsersQuery = { __typename?: 'Query', result: { __typename?: 'QueryUsersConnection', edges: Array<{ __typename?: 'QueryUsersConnectionEdge', node: { __typename?: 'User', username: string } }> } }; - - -export const Test_MyMembershipDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Test_MyMembership"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"slug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"group"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Group"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"myMembership"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_MembershipsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Test_Memberships"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"slug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"group"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Group"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"memberships"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_CreateGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Test_CreateGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateGroupInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateGroupResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_CreateModelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Test_CreateModel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateSquiggleSnippetModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createSquiggleSnippetModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSquiggleSnippetModelResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_InviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Test_Invite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationInviteUserToGroupInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"inviteUserToGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InviteUserToGroupResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invite"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_AcceptInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Test_AcceptInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationReactToGroupInviteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"reactToGroupInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReactToGroupInviteResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreateGroupTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateGroupTest"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"slug"},"value":{"kind":"StringValue","value":"testgroup","block":false}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateGroupResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"memberships"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreateSquiggleSnippetModelTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSquiggleSnippetModelTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateSquiggleSnippetModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createSquiggleSnippetModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ValidationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSquiggleSnippetModelResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"isPrivate"}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const DeleteMembershipTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteMembershipTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"group"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"user"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"deleteMembership"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"group"},"value":{"kind":"Variable","name":{"kind":"Name","value":"group"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"user"},"value":{"kind":"Variable","name":{"kind":"Name","value":"user"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteMembershipResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]}}]} as unknown as DocumentNode; -export const DeleteModelTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteModelTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationDeleteModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"deleteModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotFoundError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteModelResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]}}]} as unknown as DocumentNode; -export const InviteTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InviteTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationInviteUserToGroupInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"inviteUserToGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InviteUserToGroupResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invite"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const SetUsernameTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetUsernameTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"username"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"setUsername"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"username"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ValidationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Me"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]} as unknown as DocumentNode; -export const TestGroupsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestGroups"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GroupsQueryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"groups"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"10"}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const TestMeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestMe"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode; -export const TestModelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"isEditable"}},{"kind":"Field","name":{"kind":"Name","value":"isPrivate"}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const TestModels_CreateModelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TestModels_createModel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateSquiggleSnippetModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createSquiggleSnippetModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}}]} as unknown as DocumentNode; -export const TestUserByUsernameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestUserByUsername"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"username"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"userByUsername"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"username"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode; -export const TestUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestUsers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UsersQueryInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/hub/test/gql-gen/index.ts b/packages/hub/test/gql-gen/index.ts deleted file mode 100644 index f51599168f..0000000000 --- a/packages/hub/test/gql-gen/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./fragment-masking"; -export * from "./gql"; \ No newline at end of file diff --git a/packages/hub/test/graphql/commonQueries.ts b/packages/hub/test/graphql/commonQueries.ts deleted file mode 100644 index 0e4b5886a0..0000000000 --- a/packages/hub/test/graphql/commonQueries.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { graphql } from "../gql-gen"; -import { GroupInviteReaction, MembershipRole } from "../gql-gen/graphql"; -import { - executeCommonOperation, - setCurrentUser, - unsetCurrentUser, -} from "./helpers"; - -export const commonTestQueries = { - myMembership: async (groupSlug: string) => { - const query = graphql(/* GraphQL */ ` - query Test_MyMembership($slug: String!) { - result: group(slug: $slug) { - __typename - ... on Group { - id - myMembership { - id - role - } - } - } - } - `); - const group = await executeCommonOperation(query, { - variables: { slug: groupSlug }, - expectedTypename: "Group", - }); - return group?.myMembership; - }, - - memberships: async (groupSlug: string) => { - const query = graphql(/* GraphQL */ ` - query Test_Memberships($slug: String!) { - result: group(slug: $slug) { - __typename - ... on Group { - id - memberships { - edges { - node { - id - role - user { - slug - } - } - } - } - } - } - } - `); - const group = await executeCommonOperation(query, { - variables: { slug: groupSlug }, - expectedTypename: "Group", - }); - return group?.memberships.edges.map((edge) => edge.node); - }, -}; - -async function createGroup(slug: string) { - const mutation = graphql(/* GraphQL */ ` - mutation Test_CreateGroup($input: MutationCreateGroupInput!) { - result: createGroup(input: $input) { - __typename - ... on Error { - message - } - ... on CreateGroupResult { - group { - id - } - } - } - } - `); - - return await executeCommonOperation(mutation, { - variables: { input: { slug } }, - expectedTypename: "CreateGroupResult", - }); -} - -async function createModel({ - slug, - groupSlug, -}: { - slug: string; - groupSlug?: string; -}) { - const mutation = graphql(/* GraphQL */ ` - mutation Test_CreateModel( - $input: MutationCreateSquiggleSnippetModelInput! - ) { - result: createSquiggleSnippetModel(input: $input) { - __typename - ... on Error { - message - } - ... on CreateSquiggleSnippetModelResult { - model { - id - } - } - } - } - `); - - return await executeCommonOperation(mutation, { - variables: { input: { groupSlug, slug, code: "2+2", version: "dev" } }, - expectedTypename: "CreateSquiggleSnippetModelResult", - }); -} - -// note that the user is unset after this function -async function addMember({ - group, - admin, - user, - role, -}: { - // all of group, admin, user must already exist - group: string; - admin: string; - user: string; - role: MembershipRole; -}) { - await setCurrentUser(admin); - - const { invite } = await executeCommonOperation( - graphql(/* GraphQL */ ` - mutation Test_Invite($input: MutationInviteUserToGroupInput!) { - result: inviteUserToGroup(input: $input) { - __typename - ... on BaseError { - message - } - ... on InviteUserToGroupResult { - invite { - id - } - } - } - } - `), - { - variables: { - input: { group, username: user, role }, - }, - expectedTypename: "InviteUserToGroupResult", - } - ); - - await setCurrentUser(user); - await executeCommonOperation( - graphql(/* GraphQL */ ` - mutation Test_AcceptInvite($input: MutationReactToGroupInviteInput!) { - result: reactToGroupInvite(input: $input) { - __typename - ... on BaseError { - message - } - ... on ReactToGroupInviteResult { - __typename - } - } - } - `), - { - variables: { - input: { inviteId: invite.id, action: GroupInviteReaction.Accept }, - }, - expectedTypename: "ReactToGroupInviteResult", - } - ); - await unsetCurrentUser(); -} - -// These are optimized for convenience; their signatures don't match the underlying mutation's signature. -// Tests use these mutations when testing the mutation is not the goal of a test; instead, mutations are used to create underlying data for testing other queries and mutations. -export const commonTestMutations = { - createGroup, - createModel, - addMember, -}; diff --git a/packages/hub/test/graphql/helpers.ts b/packages/hub/test/graphql/helpers.ts deleted file mode 100644 index 56f9ce9872..0000000000 --- a/packages/hub/test/graphql/helpers.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; -import { ExecutionResult, print } from "graphql"; -import { createYoga } from "graphql-yoga"; -import { Session } from "next-auth"; - -import { schema } from "@/graphql/schema"; -import { prisma } from "@/prisma"; - -let currentUser: Session["user"] | null; - -// Useful when you need a custom email or to create a user without username. -// In other cases, `setCurrentUser` function below is more convenient. -export async function setCurrentUserObject(user: { - email: string; - username?: string; -}) { - await prisma.user.upsert({ - where: { - email: user.email, - }, - create: { - email: user.email, - ...(user.username - ? { - asOwner: { - create: { - slug: user.username, - }, - }, - } - : {}), - }, - update: {}, - }); - currentUser = user; -} - -export async function setCurrentUser(username: string) { - await setCurrentUserObject({ - email: `${username}@example.com`, - username, - }); -} - -export async function unsetCurrentUser() { - currentUser = null; -} - -beforeEach(unsetCurrentUser); - -const yoga = createYoga({ - schema, - context: async () => { - const session: Session | null = currentUser - ? { - user: currentUser, - expires: "mock", - } - : null; - return { session }; - }, -}); - -// Note: buildHTTPExecutor from @graphql-tools/executor-http, as described in -// https://the-guild.dev/graphql/yoga-server/docs/features/testing, might be more flexible, -// but its executor returns an MaybeAsyncIterable that's hard to unpack. -export async function executeOperation( - operation: TypedDocumentNode, - variables?: TVariables -): Promise["data"]>> { - const response = await Promise.resolve( - yoga.fetch("http://yoga/graphql", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - query: print(operation), - variables: variables ?? undefined, - }), - }) - ); - const yogaResponse = await response.json(); - - if (!yogaResponse.data) { - throw new Error("No data"); - } - return yogaResponse.data; -} - -type CommonResult = { - // see also: `useAsyncMutation` hook - readonly result: { - readonly __typename: string; - }; -}; - -export async function executeCommonOperation< - TResult extends CommonResult, - const TExpectedTypename extends string, - TVariables, ->( - operation: TypedDocumentNode, - options: { - variables?: TVariables; - expectedTypename: TExpectedTypename; - } -): Promise< - Extract -> { - const { result } = await executeOperation( - operation, - options.variables - ); - - if (result.__typename === options.expectedTypename) { - // we want to return a result before "BaseError" check, because it's valid for the test to expect an error - return result as Extract< - TResult["result"], - { readonly __typename: TExpectedTypename } - >; - } - - if (result.__typename === "BaseError") { - throw new Error( - (result as { message?: string })?.message ?? - `Typename mismatch: ${result.__typename}` - ); - } - - throw new Error(`Unexpected result: ${result.__typename}`); -} - -// returns two wrappers on top of executeCommonOperation -export function createRunners< - TResult extends CommonResult, - const TExpectedTypename extends string, - TVariables, ->( - mutation: TypedDocumentNode, - expectedTypename: TExpectedTypename -) { - const runOk = async (variables: TVariables) => { - return await executeCommonOperation(mutation, { - variables, - expectedTypename, - }); - }; - - const runError = async ( - variables: TVariables, - typename: T - ) => { - return await executeCommonOperation(mutation, { - variables, - expectedTypename: typename, - }); - }; - - return { runOk, runError }; -} - -// common convenient speicalization of createRunners - functions accept input instead of variables -export function createInputRunners< - TResult extends CommonResult, - const TExpectedTypename extends string, - TInput, ->( - mutation: TypedDocumentNode, - expectedTypename: TExpectedTypename -) { - const { runOk, runError } = createRunners(mutation, expectedTypename); - return { - runOk: (input: TInput) => runOk({ input }), - runError: (input: TInput, typename: T) => - runError({ input }, typename), - }; -} diff --git a/packages/hub/test/graphql/mutations/createGroup.test.ts b/packages/hub/test/graphql/mutations/createGroup.test.ts deleted file mode 100644 index 673f77bdf4..0000000000 --- a/packages/hub/test/graphql/mutations/createGroup.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { createRunners, setCurrentUser } from "../helpers"; - -const CreateGroupTest = graphql(/* GraphQL */ ` - mutation CreateGroupTest { - result: createGroup(input: { slug: "testgroup" }) { - __typename - ... on CreateGroupResult { - group { - id - slug - memberships { - edges { - node { - user { - username - } - role - } - } - } - } - } - ... on BaseError { - message - } - } - } -`); - -const { runOk, runError } = createRunners(CreateGroupTest, "CreateGroupResult"); - -test("no auth", async () => { - const result = await runError({}, "BaseError"); - expect(result.message).toMatch("Not authorized"); -}); - -test("basic", async () => { - await setCurrentUser("mockuser"); - const result = await runOk({}); - - expect(result).toMatchObject({ - group: { - slug: "testgroup", - }, - }); - expect(result.group.memberships).toEqual({ - edges: [ - { - node: { - user: { - username: "mockuser", - }, - role: "Admin", - }, - }, - ], - }); -}); diff --git a/packages/hub/test/graphql/mutations/createSquiggleSnippetModel.test.ts b/packages/hub/test/graphql/mutations/createSquiggleSnippetModel.test.ts deleted file mode 100644 index c318d30ded..0000000000 --- a/packages/hub/test/graphql/mutations/createSquiggleSnippetModel.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { commonTestMutations } from "../commonQueries"; -import { createInputRunners, setCurrentUser } from "../helpers"; - -const Mutation = graphql(/* GraphQL */ ` - mutation CreateSquiggleSnippetModelTest( - $input: MutationCreateSquiggleSnippetModelInput! - ) { - result: createSquiggleSnippetModel(input: $input) { - __typename - ... on Error { - message - } - ... on ValidationError { - issues { - message - } - } - ... on CreateSquiggleSnippetModelResult { - model { - id - slug - isPrivate - owner { - __typename - slug - } - } - } - } - } -`); - -const { runOk, runError } = createInputRunners( - Mutation, - "CreateSquiggleSnippetModelResult" -); - -test("no auth", async () => { - const result = await runError( - { code: "2+2", slug: "testmodel", version: "dev" }, - "BaseError" - ); - expect(result.message).toMatch("Not authorized"); -}); - -test("bad slug", async () => { - await setCurrentUser("mockuser"); - const result = await runError( - { code: "2+2", slug: "foo bar", version: "dev" }, - "ValidationError" - ); - expect(result.message).toMatch("[input.slug] Must be alphanumerical"); -}); - -test("basic", async () => { - await setCurrentUser("mockuser"); - const result = await runOk({ - code: "2+2", - slug: "testmodel", - version: "dev", - }); - - expect(result.model.slug).toBe("testmodel"); - expect(result.model.owner.__typename).toBe("User"); - expect(result.model.owner.slug).toBe("mockuser"); - expect(result.model.isPrivate).toBe(false); -}); - -test("private", async () => { - await setCurrentUser("mockuser"); - const result = await runOk({ - code: "2+2", - slug: "testmodel", - version: "dev", - isPrivate: true, - }); - - expect(result.model.slug).toBe("testmodel"); - expect(result.model.owner.__typename).toBe("User"); - expect(result.model.owner.slug).toBe("mockuser"); - expect(result.model.isPrivate).toBe(true); -}); - -test("for group", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup("testgroup"); - - const result = await runOk({ - code: "2+2", - slug: "testmodel", - groupSlug: "testgroup", - version: "dev", - }); - - expect(result.model.owner.__typename).toBe("Group"); - expect(result.model.owner.slug).toBe("testgroup"); -}); - -test("for group with bad slug", async () => { - await setCurrentUser("mockuser"); - - const result = await runError( - { - code: "2+2", - slug: "testmodel", - groupSlug: "no such group", - version: "dev", - }, - "ValidationError" - ); - - expect(result.message).toMatch("[input.groupSlug] Must be alphanumerical"); -}); - -test("duplicate", async () => { - await setCurrentUser("mockuser"); - - await runOk({ code: "2+2", slug: "testmodel", version: "dev" }); - - const result = await runError( - { code: "2+2", slug: "testmodel", version: "dev" }, - "BaseError" - ); - - expect(result.message).toMatch( - "Model testmodel already exists on this account" - ); -}); diff --git a/packages/hub/test/graphql/mutations/deleteMembership.test.ts b/packages/hub/test/graphql/mutations/deleteMembership.test.ts deleted file mode 100644 index a56d8c8a28..0000000000 --- a/packages/hub/test/graphql/mutations/deleteMembership.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { MembershipRole } from "../../gql-gen/graphql"; -import { commonTestMutations, commonTestQueries } from "../commonQueries"; -import { createRunners, setCurrentUser } from "../helpers"; - -const { runOk, runError } = createRunners( - graphql(/* GraphQL */ ` - mutation DeleteMembershipTest($group: String!, $user: String!) { - result: deleteMembership(input: { group: $group, user: $user }) { - __typename - ... on BaseError { - message - } - ... on DeleteMembershipResult { - ok - } - } - } - `), - "DeleteMembershipResult" -); - -async function commonConfiguration() { - const group = "testgroup"; - const admin = "mockadmin"; - await setCurrentUser(admin); - await setCurrentUser("mockadmin2"); - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - - await setCurrentUser(admin); - await commonTestMutations.createGroup(group); - - await commonTestMutations.addMember({ - group, - admin, - user: "mockuser", - role: MembershipRole.Member, - }); - await commonTestMutations.addMember({ - group, - admin, - user: "mockuser2", - role: MembershipRole.Member, - }); - await commonTestMutations.addMember({ - group, - admin, - user: "mockadmin2", - role: MembershipRole.Admin, - }); -} - -test("member deletes self", async () => { - await commonConfiguration(); - - // sanity check - expect( - (await commonTestQueries.memberships("testgroup")) - .map((m) => m.user.slug) - .sort() - ).toEqual(["mockadmin", "mockadmin2", "mockuser", "mockuser2"].sort()); - - await setCurrentUser("mockuser"); - - const result = await runOk({ - group: "testgroup", - user: "mockuser", - }); - expect(result.ok).toBe(true); - - expect( - (await commonTestQueries.memberships("testgroup")) - .map((m) => m.user.slug) - .sort() - ).toEqual(["mockadmin", "mockadmin2", "mockuser2"].sort()); -}); - -test("admin deletes member", async () => { - await commonConfiguration(); - - await setCurrentUser("mockadmin"); - - const result = await runOk({ - group: "testgroup", - user: "mockuser", - }); - expect(result.ok).toBe(true); - - expect( - (await commonTestQueries.memberships("testgroup")) - .map((m) => m.user.slug) - .sort() - ).toEqual(["mockadmin", "mockadmin2", "mockuser2"].sort()); -}); - -test("member can't delete admin", async () => { - await commonConfiguration(); - - await setCurrentUser("mockuser"); - - const result = await runError( - { - group: "testgroup", - user: "mockadmin", - }, - "BaseError" - ); - expect(result.message).toBe("Only admins can delete other members"); - - expect( - (await commonTestQueries.memberships("testgroup")) - .map((m) => m.user.slug) - .sort() - ).toEqual(["mockadmin", "mockadmin2", "mockuser", "mockuser2"].sort()); -}); - -test("no such member", async () => { - await commonConfiguration(); - - await setCurrentUser("another-user"); - await setCurrentUser("mockadmin"); - - const result = await runError( - { - group: "testgroup", - user: "another-user", - }, - "BaseError" - ); - expect(result.message).toBe("another-user is not a member of testgroup"); -}); - -test("not signed in", async () => { - await commonConfiguration(); - const result = await runError( - { group: "testgroup", user: "mockadmin" }, - "BaseError" - ); - expect(result.message).toMatch("Not authorized"); -}); - -test("not a member", async () => { - await commonConfiguration(); - await setCurrentUser("another-user"); - const result = await runError( - { group: "testgroup", user: "mockadmin" }, - "BaseError" - ); - expect(result.message).toBe("You're not a member of this group"); -}); - -test("delete self as last user", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup("testgroup"); - const result = await runError( - { group: "testgroup", user: "mockuser" }, - "BaseError" - ); - expect(result.message).toBe( - "Can't delete, mockuser is the last admin of testgroup" - ); -}); diff --git a/packages/hub/test/graphql/mutations/deleteModel.test.ts b/packages/hub/test/graphql/mutations/deleteModel.test.ts deleted file mode 100644 index 859038e6f6..0000000000 --- a/packages/hub/test/graphql/mutations/deleteModel.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { commonTestMutations } from "../commonQueries"; -import { createInputRunners, setCurrentUser } from "../helpers"; - -const Mutation = graphql(/* GraphQL */ ` - mutation DeleteModelTest($input: MutationDeleteModelInput!) { - result: deleteModel(input: $input) { - __typename - ... on Error { - message - } - ... on NotFoundError { - message - } - ... on DeleteModelResult { - ok - } - } - } -`); - -const { runOk, runError } = createInputRunners(Mutation, "DeleteModelResult"); - -test("no auth", async () => { - const result = await runError( - { owner: "testuser", slug: "testmodel" }, - "BaseError" - ); - expect(result.message).toMatch("Not authorized"); -}); - -test("no such model", async () => { - await setCurrentUser("mockuser"); - const result = await runError( - { owner: "testuser", slug: "testmodel" }, - "NotFoundError" - ); - expect(result.message).toMatch("Can't find model"); -}); - -test("ok", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createModel({ slug: "testmodel" }); - - const result = await runOk({ owner: "mockuser", slug: "testmodel" }); - expect(result.ok).toBe(true); -}); - -test("double delete", async () => { - const owner = "mockuser"; - const slug = "testmodel"; - await setCurrentUser(owner); - await commonTestMutations.createModel({ slug }); - - await runOk({ owner, slug }); - const result = await runError({ owner, slug }, "NotFoundError"); - - expect(result.message).toMatch("Can't find model"); -}); - -test("wrong user", async () => { - const owner = "mockuser"; - const otherUser = "user2"; - await setCurrentUser(owner); - await commonTestMutations.createModel({ slug: "testmodel" }); - await setCurrentUser(otherUser); - - const result = await runError( - { owner, slug: "testmodel" }, - "NotFoundError" // TODO - error should be more specific - ); - expect(result.message).toMatch("Can't find model"); -}); - -test("deleting group model", async () => { - const groupSlug = "group1"; - - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup(groupSlug); - await commonTestMutations.createModel({ groupSlug, slug: "testmodel" }); - - const result = await runOk({ owner: groupSlug, slug: "testmodel" }); - expect(result.ok).toBe(true); -}); diff --git a/packages/hub/test/graphql/mutations/inviteUserToGroup.test.ts b/packages/hub/test/graphql/mutations/inviteUserToGroup.test.ts deleted file mode 100644 index 31b6cb2954..0000000000 --- a/packages/hub/test/graphql/mutations/inviteUserToGroup.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { MembershipRole } from "../../gql-gen/graphql"; -import { commonTestMutations } from "../commonQueries"; -import { - createInputRunners, - setCurrentUser, - unsetCurrentUser, -} from "../helpers"; - -const ownerUser = "mockowner"; -const memberUser = "mockmember"; - -async function prepareGroup() { - await setCurrentUser(ownerUser); - await commonTestMutations.createGroup("testgroup"); -} - -const { runOk, runError } = createInputRunners( - graphql(/* GraphQL */ ` - mutation InviteTest($input: MutationInviteUserToGroupInput!) { - result: inviteUserToGroup(input: $input) { - __typename - ... on BaseError { - message - } - ... on InviteUserToGroupResult { - invite { - id - role - } - } - } - } - `), - "InviteUserToGroupResult" -); - -test("basic invite", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); // create a user - await setCurrentUser(ownerUser); - const result = await runOk({ - group: "testgroup", - username: "mockmember", - role: MembershipRole.Member, - }); - expect(result.invite.role).toBe("Member"); -}); - -test("no auth", async () => { - await prepareGroup(); - await unsetCurrentUser(); - - const result = await runError( - { - group: "testgroup", - username: "mockmember", - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("Not authorized"); -}); - -test("invite to another's group", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); - - const result = await runError( - { - group: "testgroup", - username: "mockmember", - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("You're not a member of testgroup group"); -}); - -test("invite to nonexistent group", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); // create a user - await setCurrentUser(ownerUser); - - const result = await runError( - { - group: "nosuchgroup", - username: "mockmember", - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("Group nosuchgroup not found"); -}); - -test("invite nonexistent user", async () => { - await prepareGroup(); - await setCurrentUser(ownerUser); - - const result = await runError( - { - group: "testgroup", - username: "nosuchuser", - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("Invited user nosuchuser not found"); -}); - -test("existing invite", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); // create a user - await setCurrentUser(ownerUser); - - await runOk({ - group: "testgroup", - username: memberUser, - role: MembershipRole.Member, - }); - const result = await runError( - { - group: "testgroup", - username: memberUser, - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch( - "There's already a pending invite for mockmember to join testgroup" - ); -}); - -test("already a member", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); - await setCurrentUser(ownerUser); - await commonTestMutations.addMember({ - group: "testgroup", - admin: ownerUser, - user: memberUser, - role: MembershipRole.Member, - }); - - await setCurrentUser(ownerUser); - const result = await runError( - { - group: "testgroup", - username: memberUser, - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("mockmember is already a member of testgroup"); -}); diff --git a/packages/hub/test/graphql/mutations/setUsername.test.ts b/packages/hub/test/graphql/mutations/setUsername.test.ts deleted file mode 100644 index c7cc601294..0000000000 --- a/packages/hub/test/graphql/mutations/setUsername.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { createRunners, setCurrentUserObject } from "../helpers"; - -const SetUsernameTest = graphql(/* GraphQL */ ` - mutation SetUsernameTest($username: String!) { - result: setUsername(username: $username) { - __typename - ... on Error { - message - } - ... on ValidationError { - message - } - ... on Me { - email - username - } - } - } -`); - -const { runOk, runError } = createRunners(SetUsernameTest, "Me"); - -test("no auth", async () => { - const result = await runError({ username: "mockuser" }, "BaseError"); - expect(result.message).toMatch("Not authorized"); -}); - -test("already set", async () => { - await setCurrentUserObject({ - email: "mock@example.com", - username: "mockuser", - }); - - const result = await runError({ username: "mockuser2" }, "BaseError"); - expect(result.message).toMatch("Username is already set"); -}); - -test("bad username", async () => { - await setCurrentUserObject({ email: "mock@example.com" }); - const result = await runError({ username: "foo bar" }, "ValidationError"); - expect(result.message).toMatch("[username] Must be alphanumerical"); -}); - -test("not available", async () => { - await setCurrentUserObject({ email: "mock@example.com" }); - await runOk({ username: "mockuser" }); - - await setCurrentUserObject({ email: "mock2@example.com" }); - const result = await runError({ username: "mockuser" }, "BaseError"); - - expect(result.message).toMatch("Username mockuser is not available"); -}); - -test("basic", async () => { - await setCurrentUserObject({ email: "mock@example.com" }); - const result = await runOk({ username: "mockuser" }); - - expect(result).toMatchObject({ - email: "mock@example.com", - username: "mockuser", - }); -}); diff --git a/packages/hub/test/graphql/queries/groups.test.ts b/packages/hub/test/graphql/queries/groups.test.ts deleted file mode 100644 index 62f1d953cc..0000000000 --- a/packages/hub/test/graphql/queries/groups.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { commonTestMutations } from "../commonQueries"; -import { createRunners, setCurrentUser, unsetCurrentUser } from "../helpers"; - -const { runOk, runError } = createRunners( - graphql(/* GraphQL */ ` - query TestGroups($input: GroupsQueryInput!) { - result: groups(first: 10, input: $input) { - __typename - edges { - node { - id - slug - } - } - } - } - `), - "GroupConnection" -); - -test("list groups", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup("group1"); - await commonTestMutations.createGroup("group2"); - await commonTestMutations.createGroup("group3"); - const result = await runOk({ input: {} }); - - expect(result.edges.length).toEqual(3); -}); - -test("slugContains", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup("foo-bar"); - await commonTestMutations.createGroup("foo-baz"); - await commonTestMutations.createGroup("bar-baz"); - const result = await runOk({ input: { slugContains: "bar" } }); - - expect(result.edges.length).toEqual(2); - expect(result.edges.map((e) => e.node.slug).sort()).toEqual( - ["foo-bar", "bar-baz"].sort() - ); -}); - -test("myOnly", async () => { - await setCurrentUser("user1"); - await commonTestMutations.createGroup("group1"); - await commonTestMutations.createGroup("group4"); - - await setCurrentUser("user2"); - await commonTestMutations.createGroup("group2"); - await commonTestMutations.createGroup("group3"); - - const result2 = await runOk({ input: { myOnly: true } }); - - expect(result2.edges.length).toEqual(2); - expect(result2.edges.map((e) => e.node.slug).sort()).toEqual( - ["group2", "group3"].sort() - ); - - await setCurrentUser("user1"); - const result1 = await runOk({ input: { myOnly: true } }); - expect(result1.edges.length).toEqual(2); - expect(result1.edges.map((e) => e.node.slug).sort()).toEqual( - ["group1", "group4"].sort() - ); -}); - -test("myOnly not signed in", async () => { - await setCurrentUser("user1"); - await commonTestMutations.createGroup("group1"); - await commonTestMutations.createGroup("group4"); - await unsetCurrentUser(); - const result = await runOk({ input: { myOnly: true } }); - expect(result.edges.length).toEqual(0); -}); diff --git a/packages/hub/test/graphql/queries/me.test.ts b/packages/hub/test/graphql/queries/me.test.ts deleted file mode 100644 index 2f3ac758b2..0000000000 --- a/packages/hub/test/graphql/queries/me.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { executeOperation, setCurrentUserObject } from "../helpers"; - -test("basic", async () => { - await setCurrentUserObject({ - email: "mock@example.com", - username: "mockuser", - }); - const { me } = await executeOperation( - graphql(/* GraphQL */ ` - query TestMe { - me { - __typename - email - username - } - } - `) - ); - - expect(me.email).toBe("mock@example.com"); - expect(me.username).toBe("mockuser"); -}); diff --git a/packages/hub/test/graphql/queries/models.test.ts b/packages/hub/test/graphql/queries/models.test.ts deleted file mode 100644 index 980db63810..0000000000 --- a/packages/hub/test/graphql/queries/models.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { - executeCommonOperation, - executeOperation, - setCurrentUser, - unsetCurrentUser, -} from "../helpers"; - -const Query = graphql(/* GraphQL */ ` - query TestModels { - models { - edges { - node { - slug - isEditable - isPrivate - owner { - __typename - slug - } - } - } - } - } -`); - -const CreateModel = graphql(/* GraphQL */ ` - mutation TestModels_createModel( - $input: MutationCreateSquiggleSnippetModelInput! - ) { - result: createSquiggleSnippetModel(input: $input) { - __typename - } - } -`); - -test("empty", async () => { - const { models } = await executeOperation(Query); - expect(models.edges.length).toBe(0); -}); - -const user = "mockuser"; - -async function createModel(slug: string, options: { private?: boolean } = {}) { - await executeCommonOperation(CreateModel, { - variables: { - input: { - code: "2+2", - slug, - version: "dev", - ...(options.private ? { isPrivate: true } : {}), - }, - }, - expectedTypename: "CreateSquiggleSnippetModelResult", - }); -} - -test("list models", async () => { - await setCurrentUser(user); - await createModel("model1"); - await createModel("model2"); - await createModel("model3"); - const { models } = await executeOperation(Query); - - expect(models.edges.length).toBe(3); -}); - -test("default limit", async () => { - await setCurrentUser(user); - for (let i = 0; i < 30; i++) { - await createModel(`model${i}`); - } - const { models } = await executeOperation(Query); - - expect(models.edges.length).toBe(20); -}); - -test("list private models", async () => { - await setCurrentUser(user); - await createModel("model1"); - await createModel("model2"); - await createModel("model3", { private: true }); - const { models } = await executeOperation(Query); - - expect(models.edges.map((edge) => edge.node.slug)).toEqual([ - "model3", - "model2", - "model1", - ]); -}); - -test("hide private models from anon users", async () => { - await setCurrentUser(user); - await createModel("model1"); - await createModel("model2", { private: true }); - await createModel("model3"); - unsetCurrentUser(); - const { models } = await executeOperation(Query); - - expect(models.edges.length).toBe(2); - expect(models.edges.map((edge) => edge.node.slug)).toEqual([ - "model3", - "model1", - ]); -}); - -test("hide private models from other users", async () => { - await setCurrentUser(user); - await createModel("model1"); - await createModel("model2", { private: true }); - await createModel("model3"); - await setCurrentUser("otheruser"); - const { models } = await executeOperation(Query); - - expect(models.edges.length).toBe(2); - expect(models.edges.map((edge) => edge.node.slug)).toEqual([ - "model3", - "model1", - ]); -}); diff --git a/packages/hub/test/graphql/queries/userByUsername.test.ts b/packages/hub/test/graphql/queries/userByUsername.test.ts deleted file mode 100644 index 90635cc268..0000000000 --- a/packages/hub/test/graphql/queries/userByUsername.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { executeCommonOperation, setCurrentUser } from "../helpers"; - -const query = graphql(/* GraphQL */ ` - query TestUserByUsername($username: String!) { - result: userByUsername(username: $username) { - __typename - ... on User { - slug - username - } - ... on Error { - message - } - } - } -`); - -test("find self", async () => { - await setCurrentUser("mockuser"); - const user = await executeCommonOperation(query, { - variables: { username: "mockuser" }, - expectedTypename: "User", - }); - - expect(user.slug).toBe("mockuser"); - expect(user.username).toBe("mockuser"); -}); - -test("find other user", async () => { - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - await setCurrentUser("mockuser3"); - const user = await executeCommonOperation(query, { - variables: { username: "mockuser2" }, - expectedTypename: "User", - }); - - expect(user.slug).toBe("mockuser2"); - expect(user.username).toBe("mockuser2"); -}); - -test("not found", async () => { - await setCurrentUser("mockuser"); - const error = await executeCommonOperation(query, { - variables: { username: "mockuser2" }, - expectedTypename: "NotFoundError", - }); - - expect(error.message).toBe("User mockuser2 not found"); -}); diff --git a/packages/hub/test/graphql/queries/users.test.ts b/packages/hub/test/graphql/queries/users.test.ts deleted file mode 100644 index 80c109bdcf..0000000000 --- a/packages/hub/test/graphql/queries/users.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { - executeOperation, - setCurrentUser, - setCurrentUserObject, -} from "../helpers"; - -const query = graphql(/* GraphQL */ ` - query TestUsers($input: UsersQueryInput) { - result: users(input: $input) { - edges { - node { - username - } - } - } - } -`); - -test("find users", async () => { - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - const { result: users } = await executeOperation(query); - - expect(users.edges.length).toBe(2); -}); - -test("skip users without usernames", async () => { - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - await setCurrentUserObject({ email: "mockuser3@example.com" }); - const { result: users } = await executeOperation(query); - - expect(users.edges.length).toBe(2); -}); - -test("filter by username", async () => { - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - await setCurrentUserObject({ email: "mockuser3@example.com" }); - await setCurrentUser("unrelated"); - const { result: users } = await executeOperation(query, { - input: { usernameContains: "ock" }, - }); - - expect(users.edges.length).toBe(2); - expect(users.edges.map((e) => e.node.username).sort()).toEqual([ - "mockuser", - "mockuser2", - ]); -}); diff --git a/packages/hub/test/setup-db.ts b/packages/hub/test/setup-db.ts index 64effcef0e..cc50cfb496 100644 --- a/packages/hub/test/setup-db.ts +++ b/packages/hub/test/setup-db.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; if (!process.env["DATABASE_URL"]?.includes("quri-test")) { throw new Error("Expected quri-test database, probable misconfiguration"); diff --git a/packages/hub/tsconfig.json b/packages/hub/tsconfig.json index d613339ed0..143fcdd821 100644 --- a/packages/hub/tsconfig.json +++ b/packages/hub/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "paths": { "@/*": ["./src/*"], - "@gen/*": ["./src/__generated__/*"], }, // type check settings "plugins": [ diff --git a/packages/ui/package.json b/packages/ui/package.json index 4b8be2b1f8..9bcbbb74cd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -41,8 +41,9 @@ "types": "./dist/index.d.ts", "source": "./src/index.ts", "peerDependencies": { - "react": "^18", - "react-dom": "^18" + "react": "^18 | ^19", + "react-dom": "^18 | ^19", + "react-hook-form": "^7.53.2" }, "dependencies": { "@floating-ui/react": "^0.26.16", @@ -50,7 +51,6 @@ "clsx": "^2.1.1", "framer-motion": "^11.0.3", "react-colorful": "^5.6.1", - "react-hook-form": "^7.50.0", "react-select": "^5.8.0", "react-textarea-autosize": "8.5.4", "react-use": "^17.5.0" @@ -77,8 +77,9 @@ "postcss-cli": "^11.0.0", "prettier": "^3.3.3", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.3.1", + "react": "19.0.0-rc-66855b96-20241106", + "react-dom": "19.0.0-rc-66855b96-20241106", + "react-hook-form": "^7.53.2", "rollup-plugin-node-builtins": "^2.1.2", "storybook": "^8.1.5", "tailwindcss": "^3.4.14", diff --git a/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx b/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx index a214586461..a461cfd801 100644 --- a/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx +++ b/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx @@ -14,10 +14,11 @@ export const DropdownMenuActionItem: FC = ({ title, icon, onClick, + acting, }) => { return (
- +
); }; diff --git a/packages/ui/src/forms/common/FormInput.tsx b/packages/ui/src/forms/common/FormInput.tsx index b991b71d60..16e43f176c 100644 --- a/packages/ui/src/forms/common/FormInput.tsx +++ b/packages/ui/src/forms/common/FormInput.tsx @@ -24,7 +24,12 @@ export function FormInput< TValues extends FieldValues, TFieldName extends FieldPath = FieldPath, >({ name, rules, children }: Props) { - const { register } = useFormContext(); + const formContext = useFormContext(); + if (!formContext) { + throw new Error("FormInput must be used within a FormProvider"); + } + + const { register } = formContext; return ( name={name}> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7a9ba2bfa..547d36311f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,13 +155,13 @@ importers: version: 1.1.4 '@floating-ui/react': specifier: ^0.26.16 - version: 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.26.16(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@heroicons/react': specifier: ^1.0.6 - version: 1.0.6(react@18.3.1) + version: 1.0.6(react@19.0.0-rc-66855b96-20241106) '@hookform/resolvers': specifier: ^3.3.4 - version: 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + version: 3.3.4(react-hook-form@7.53.2(react@19.0.0-rc-66855b96-20241106)) '@lezer/common': specifier: ^1.2.2 version: 1.2.3 @@ -197,16 +197,16 @@ importers: version: 3.3.3 react-draggable: specifier: ^4.4.6 - version: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.4.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-hook-form: - specifier: ^7.50.0 - version: 7.50.0(react@18.3.1) + specifier: ^7.53.2 + version: 7.53.2(react@19.0.0-rc-66855b96-20241106) react-markdown: specifier: ^9.0.1 - version: 9.0.1(@types/react@18.3.3)(react@18.3.1) + version: 9.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) reactflow: specifier: ^11.11.4 - version: 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) remark-gfm: specifier: ^4.0.0 version: 4.0.0 @@ -249,22 +249,22 @@ importers: version: link:../configs '@storybook/addon-essentials': specifier: ^8.1.6 - version: 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/addon-links': specifier: ^8.1.6 - version: 8.1.6(react@18.3.1) + version: 8.1.6(react@19.0.0-rc-66855b96-20241106) '@storybook/react': specifier: ^8.1.6 - version: 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) '@storybook/react-vite': specifier: ^8.1.6 - version: 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7)) + version: 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3))) '@testing-library/react': specifier: ^16.0.1 - version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@10.4.0) @@ -332,17 +332,17 @@ importers: specifier: ^11.0.0 version: 11.0.0(jiti@1.21.0)(postcss@8.4.38) react: - specifier: ^18.2.0 - version: 18.3.1 + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) rollup-plugin-node-builtins: specifier: ^2.1.2 version: 2.1.2 storybook: specifier: ^8.1.6 - version: 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) tailwindcss: specifier: ^3.4.3 version: 3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3)) @@ -412,30 +412,6 @@ importers: '@auth/prisma-adapter': specifier: ^2.7.4 version: 2.7.4(@prisma/client@5.22.0(prisma@5.22.0))(nodemailer@6.9.13) - '@pothos/core': - specifier: ^3.41.1 - version: 3.41.1(graphql@16.8.1) - '@pothos/plugin-errors': - specifier: ^3.11.1 - version: 3.11.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) - '@pothos/plugin-prisma': - specifier: ^3.65.2 - version: 3.65.2(@pothos/core@3.41.1(graphql@16.8.1))(@prisma/client@5.22.0(prisma@5.22.0))(graphql@16.8.1)(typescript@5.6.3) - '@pothos/plugin-relay': - specifier: ^3.46.0 - version: 3.46.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) - '@pothos/plugin-scope-auth': - specifier: ^3.22.0 - version: 3.22.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) - '@pothos/plugin-simple-objects': - specifier: ^3.7.0 - version: 3.7.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) - '@pothos/plugin-validation': - specifier: ^3.10.1 - version: 3.10.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)(zod@3.23.8) - '@pothos/plugin-with-input': - specifier: ^3.10.1 - version: 3.10.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) '@prisma/client': specifier: 5.22.0 version: 5.22.0(prisma@5.22.0) @@ -456,7 +432,7 @@ importers: version: link:../versioned-components '@vercel/analytics': specifier: ^1.3.1 - version: 1.3.1(next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.3.1(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) base64-js: specifier: ^1.5.1 version: 1.5.1 @@ -469,12 +445,6 @@ importers: date-fns: specifier: ^3.6.0 version: 3.6.0 - graphql: - specifier: ^16.8.1 - version: 16.8.1 - graphql-yoga: - specifier: ^5.1.1 - version: 5.1.1(graphql@16.8.1) immutable: specifier: ^4.3.6 version: 4.3.6 @@ -486,10 +456,13 @@ importers: version: 4.17.21 next: specifier: ^15.0.3 - version: 15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) next-auth: specifier: 5.0.0-beta.25 - version: 5.0.0-beta.25(next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.13)(react@18.3.1) + version: 5.0.0-beta.25(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(nodemailer@6.9.13)(react@19.0.0-rc-66855b96-20241106) + next-safe-action: + specifier: ^7.9.9 + version: 7.9.9(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(zod@3.23.8) nodemailer: specifier: ^6.9.13 version: 6.9.13 @@ -497,51 +470,39 @@ importers: specifier: ^2.1.0 version: 2.1.0 react: - specifier: ^18.2.0 - version: 18.3.1 + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) react-hook-form: - specifier: ^7.50.0 - version: 7.50.0(react@18.3.1) + specifier: ^7.53.2 + version: 7.53.2(react@19.0.0-rc-66855b96-20241106) react-icons: specifier: ^5.2.1 - version: 5.2.1(react@18.3.1) + version: 5.2.1(react@19.0.0-rc-66855b96-20241106) react-loading-skeleton: specifier: ^3.4.0 - version: 3.4.0(react@18.3.1) + version: 3.4.0(react@19.0.0-rc-66855b96-20241106) react-markdown: specifier: ^9.0.1 - version: 9.0.1(@types/react@18.3.3)(react@18.3.1) - react-relay: - specifier: ^16.2.0 - version: 16.2.0(react@18.3.1) + version: 9.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) react-select: specifier: ^5.8.0 - version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - relay-runtime: - specifier: ^16.2.0 - version: 16.2.0 + version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) remark-breaks: specifier: ^4.0.0 version: 4.0.0 remark-gfm: specifier: ^4.0.0 version: 4.0.0 + server-only: + specifier: ^0.0.1 + version: 0.0.1 zod: specifier: ^3.23.8 version: 3.23.8 devDependencies: - '@graphql-codegen/cli': - specifier: ^5.0.2 - version: 5.0.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(enquirer@2.3.6)(graphql@16.8.1)(typescript@5.6.3) - '@graphql-codegen/client-preset': - specifier: ^4.2.5 - version: 4.2.5(graphql@16.8.1) - '@graphql-typed-document-node/core': - specifier: ^3.2.0 - version: 3.2.0(graphql@16.8.1) '@parcel/watcher': specifier: ^2.4.1 version: 2.4.1 @@ -569,15 +530,6 @@ importers: '@types/react': specifier: ^18.3.3 version: 18.3.3 - '@types/react-relay': - specifier: ^16.0.6 - version: 16.0.6 - '@types/relay-runtime': - specifier: ^14.1.23 - version: 14.1.23 - babel-plugin-relay: - specifier: ^16.2.0 - version: 16.2.0 dotenv-cli: specifier: ^7.4.2 version: 7.4.2 @@ -608,9 +560,6 @@ importers: prisma: specifier: ^5.22.0 version: 5.22.0 - relay-compiler: - specifier: ^16.2.0 - version: 16.2.0 tailwindcss: specifier: ^3.4.3 version: 3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3)) @@ -871,31 +820,28 @@ importers: dependencies: '@floating-ui/react': specifier: ^0.26.16 - version: 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.26.16(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@headlessui/react': specifier: ^2.2.0 - version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.2.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) clsx: specifier: ^2.1.1 version: 2.1.1 framer-motion: specifier: ^11.0.3 - version: 11.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 11.0.3(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-colorful: specifier: ^5.6.1 - version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: - specifier: ^7.50.0 - version: 7.50.0(react@18.3.1) + version: 5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-select: specifier: ^5.8.0 - version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-textarea-autosize: specifier: 8.5.4 - version: 8.5.4(@types/react@18.3.3)(react@18.3.1) + version: 8.5.4(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) react-use: specifier: ^17.5.0 - version: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 17.5.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) devDependencies: '@quri/configs': specifier: workspace:* @@ -905,22 +851,22 @@ importers: version: 8.1.6(@types/react-dom@18.3.0)(prettier@3.3.3) '@storybook/addon-essentials': specifier: ^8.1.5 - version: 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/addon-interactions': specifier: ^8.1.5 version: 8.1.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3))) '@storybook/addon-links': specifier: ^8.0.9 - version: 8.0.9(react@18.3.1) + version: 8.0.9(react@19.0.0-rc-66855b96-20241106) '@storybook/blocks': specifier: ^8.0.9 - version: 8.0.9(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.0.9(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/react': specifier: ^8.1.5 - version: 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) '@storybook/react-vite': specifier: ^8.0.9 - version: 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0)) + version: 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0)) '@storybook/testing-library': specifier: ^0.2.2 version: 0.2.2 @@ -961,17 +907,20 @@ importers: specifier: ^15.8.1 version: 15.8.1 react: - specifier: ^18.2.0 - version: 18.3.1 + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-hook-form: + specifier: ^7.53.2 + version: 7.53.2(react@19.0.0-rc-66855b96-20241106) rollup-plugin-node-builtins: specifier: ^2.1.2 version: 2.1.2 storybook: specifier: ^8.1.5 - version: 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) tailwindcss: specifier: ^3.4.14 version: 3.4.14(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) @@ -1342,16 +1291,6 @@ packages: '@anthropic-ai/sdk@0.29.2': resolution: {integrity: sha512-5dwiOPO/AZvhY4bJIG9vjFKU9Kza3hA6VEsbIQg6L9vny2RQIpCFhV50nB9IrG2edZaHZb4HuQ9Wmsn5zgWyZg==} - '@ardatan/relay-compiler@12.0.0': - resolution: {integrity: sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==} - hasBin: true - peerDependencies: - graphql: '*' - - '@ardatan/sync-fetch@0.0.1': - resolution: {integrity: sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA==} - engines: {node: '>=14'} - '@auth/core@0.37.2': resolution: {integrity: sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==} peerDependencies: @@ -1453,10 +1392,6 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.25.4': - resolution: {integrity: sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==} - engines: {node: '>=6.9.0'} - '@babel/compat-data@7.26.2': resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} engines: {node: '>=6.9.0'} @@ -1465,14 +1400,6 @@ packages: resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.23.6': - resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.24.1': - resolution: {integrity: sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.25.6': resolution: {integrity: sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==} engines: {node: '>=6.9.0'} @@ -1497,10 +1424,6 @@ packages: resolution: {integrity: sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.2': - resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.9': resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} @@ -1793,20 +1716,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-proposal-class-properties@7.18.6': - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-object-rest-spread@7.20.7': - resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} @@ -1834,12 +1743,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.24.7': - resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.26.0': resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} engines: {node: '>=6.9.0'} @@ -1928,12 +1831,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-arrow-functions@7.24.7': - resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-arrow-functions@7.25.9': resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} engines: {node: '>=6.9.0'} @@ -1952,24 +1849,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.24.7': - resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.25.9': resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.24.7': - resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.25.9': resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} engines: {node: '>=6.9.0'} @@ -1994,36 +1879,18 @@ packages: peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.24.7': - resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-classes@7.25.9': resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.24.7': - resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.25.9': resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.24.7': - resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.25.9': resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} engines: {node: '>=6.9.0'} @@ -2072,24 +1939,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-for-of@7.24.7': - resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-for-of@7.25.9': resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.24.7': - resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.25.9': resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} engines: {node: '>=6.9.0'} @@ -2102,12 +1957,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-literals@7.24.7': - resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-literals@7.25.9': resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} engines: {node: '>=6.9.0'} @@ -2120,12 +1969,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-member-expression-literals@7.24.7': - resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-member-expression-literals@7.25.9': resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} engines: {node: '>=6.9.0'} @@ -2204,12 +2047,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-super@7.24.7': - resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-super@7.25.9': resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} engines: {node: '>=6.9.0'} @@ -2234,12 +2071,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-parameters@7.24.7': - resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-parameters@7.25.9': resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} engines: {node: '>=6.9.0'} @@ -2264,12 +2095,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-property-literals@7.24.7': - resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-property-literals@7.25.9': resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} engines: {node: '>=6.9.0'} @@ -2318,24 +2143,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-shorthand-properties@7.24.7': - resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-shorthand-properties@7.25.9': resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.24.7': - resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.25.9': resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} engines: {node: '>=6.9.0'} @@ -2348,12 +2161,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-template-literals@7.24.7': - resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-template-literals@7.25.9': resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} engines: {node: '>=6.9.0'} @@ -2462,10 +2269,6 @@ packages: resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@babel/template@7.23.9': - resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==} - engines: {node: '>=6.9.0'} - '@babel/template@7.24.0': resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} @@ -2478,10 +2281,6 @@ packages: resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.24.1': - resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.24.5': resolution: {integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==} engines: {node: '>=6.9.0'} @@ -2758,14 +2557,6 @@ packages: '@emotion/weak-memoize@0.3.1': resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} - '@envelop/core@5.0.0': - resolution: {integrity: sha512-aJdnH/ptv+cvwfvciCBe7TSvccBwo9g0S5f6u35TBVzRVqIGkK03lFlIL+x1cnfZgN9EfR2b1PH2galrT1CdCQ==} - engines: {node: '>=18.0.0'} - - '@envelop/types@5.0.0': - resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==} - engines: {node: '>=18.0.0'} - '@esbuild-plugins/node-resolve@0.2.2': resolution: {integrity: sha512-+t5FdX3ATQlb53UFDBRb4nqjYBz492bIrnVWvpQHpzZlu9BQL5HasMZhqc409ygUwOWCXZhrWr6NyZ6T6Y+cxw==} peerDependencies: @@ -3403,227 +3194,6 @@ packages: '@content-collections/mdx': 0.x.x fumadocs-core: ^13.2.1 || ^14.0.0 - '@graphql-codegen/add@5.0.2': - resolution: {integrity: sha512-ouBkSvMFUhda5VoKumo/ZvsZM9P5ZTyDsI8LW18VxSNWOjrTeLXBWHG8Gfaai0HwhflPtCYVABbriEcOmrRShQ==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/cli@5.0.2': - resolution: {integrity: sha512-MBIaFqDiLKuO4ojN6xxG9/xL9wmfD3ZjZ7RsPjwQnSHBCUXnEkdKvX+JVpx87Pq29Ycn8wTJUguXnTZ7Di0Mlw==} - hasBin: true - peerDependencies: - '@parcel/watcher': ^2.1.0 - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - peerDependenciesMeta: - '@parcel/watcher': - optional: true - - '@graphql-codegen/client-preset@4.2.5': - resolution: {integrity: sha512-hAdB6HN8EDmkoBtr0bPUN/7NH6svzqbcTDMWBCRXPESXkl7y80po+IXrXUjsSrvhKG8xkNXgJNz/2mjwHzywcA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/core@4.0.2': - resolution: {integrity: sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/gql-tag-operations@4.0.6': - resolution: {integrity: sha512-y6iXEDpDNjwNxJw3WZqX1/Znj0QHW7+y8O+t2V8qvbTT+3kb2lr9ntc8By7vCr6ctw9tXI4XKaJgpTstJDOwFA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/plugin-helpers@5.0.3': - resolution: {integrity: sha512-yZ1rpULIWKBZqCDlvGIJRSyj1B2utkEdGmXZTBT/GVayP4hyRYlkd36AJV/LfEsVD8dnsKL5rLz2VTYmRNlJ5Q==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/schema-ast@4.0.2': - resolution: {integrity: sha512-5mVAOQQK3Oz7EtMl/l3vOQdc2aYClUzVDHHkMvZlunc+KlGgl81j8TLa+X7ANIllqU4fUEsQU3lJmk4hXP6K7Q==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/typed-document-node@5.0.6': - resolution: {integrity: sha512-US0J95hOE2/W/h42w4oiY+DFKG7IetEN1mQMgXXeat1w6FAR5PlIz4JrRrEkiVfVetZ1g7K78SOwBD8/IJnDiA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/typescript-operations@4.2.0': - resolution: {integrity: sha512-lmuwYb03XC7LNRS8oo9M4/vlOrq/wOKmTLBHlltK2YJ1BO/4K/Q9Jdv/jDmJpNydHVR1fmeF4wAfsIp1f9JibA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/typescript@4.0.6': - resolution: {integrity: sha512-IBG4N+Blv7KAL27bseruIoLTjORFCT3r+QYyMC3g11uY3/9TPpaUyjSdF70yBe5GIQ6dAgDU+ENUC1v7EPi0rw==} - peerDependencies: - graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/visitor-plugin-common@5.1.0': - resolution: {integrity: sha512-eamQxtA9bjJqI2lU5eYoA1GbdMIRT2X8m8vhWYsVQVWD3qM7sx/IqJU0kx0J3Vd4/CSd36BzL6RKwksibytDIg==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-tools/apollo-engine-loader@8.0.0': - resolution: {integrity: sha512-axQTbN5+Yxs1rJ6cWQBOfw3AEeC+fvIuZSfJLPLLvFJLj4pUm9fhxey/g6oQZAAQJqKPfw+tLDUQvnfvRK8Kmg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/batch-execute@9.0.2': - resolution: {integrity: sha512-Y2uwdZI6ZnatopD/SYfZ1eGuQFI7OU2KGZ2/B/7G9ISmgMl5K+ZZWz/PfIEXeiHirIDhyk54s4uka5rj2xwKqQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/code-file-loader@8.0.2': - resolution: {integrity: sha512-AKNpkElUL2cWocYpC4DzNEpo6qJw8Lp+L3bKQ/mIfmbsQxgLz5uve6zHBMhDaFPdlwfIox41N3iUSvi77t9e8A==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/delegate@10.0.3': - resolution: {integrity: sha512-Jor9oazZ07zuWkykD3OOhT/2XD74Zm6Ar0ENZMk75MDD51wB2UWUIMljtHxbJhV5A6UBC2v8x6iY0xdCGiIlyw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/documents@1.0.0': - resolution: {integrity: sha512-rHGjX1vg/nZ2DKqRGfDPNC55CWZBMldEVcH+91BThRa6JeT80NqXknffLLEZLRUxyikCfkwMsk6xR3UNMqG0Rg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/executor-graphql-ws@1.1.0': - resolution: {integrity: sha512-yM67SzwE8rYRpm4z4AuGtABlOp9mXXVy6sxXnTJRoYIdZrmDbKVfIY+CpZUJCqS0FX3xf2+GoHlsj7Qswaxgcg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/executor-http@1.0.2': - resolution: {integrity: sha512-JKTB4E3kdQM2/1NEcyrVPyQ8057ZVthCV5dFJiKktqY9IdmF00M8gupFcW3jlbM/Udn78ickeUBsUzA3EouqpA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/executor-legacy-ws@1.0.3': - resolution: {integrity: sha512-rr3IDeO9Dh+8u8KIro++5kzJJYPHkcrIAWzqXtN663nhInC85iW7Ko91yOYwf7ovBci/7s+4Rqe4ZRyca1LGjQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/executor@1.1.0': - resolution: {integrity: sha512-+1wmnaUHETSYxiK/ELsT60x584Rw3QKBB7F/7fJ83HKPnLifmE2Dm/K9Eyt6L0Ppekf1jNUbWBpmBGb8P5hAeg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/git-loader@8.0.2': - resolution: {integrity: sha512-AuCB0nlPvsHh8u42zRZdlD/ZMaWP9A44yAkQUVCZir1E/LG63fsZ9svTWJ+CbusW3Hd0ZP9qpxEhlHxnd4Tlsg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/github-loader@8.0.0': - resolution: {integrity: sha512-VuroArWKcG4yaOWzV0r19ElVIV6iH6UKDQn1MXemND0xu5TzrFme0kf3U9o0YwNo0kUYEk9CyFM0BYg4he17FA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/graphql-file-loader@8.0.0': - resolution: {integrity: sha512-wRXj9Z1IFL3+zJG1HWEY0S4TXal7+s1vVhbZva96MSp0kbb/3JBF7j0cnJ44Eq0ClccMgGCDFqPFXty4JlpaPg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/graphql-tag-pluck@8.0.2': - resolution: {integrity: sha512-U6fE4yEHxuk/nqmPixHpw1WhqdS6aYuaV60m1bEmUmGJNbpAhaMBy01JncpvpF15yZR5LZ0UjkHg+A3Lhoc8YQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/import@7.0.0': - resolution: {integrity: sha512-NVZiTO8o1GZs6OXzNfjB+5CtQtqsZZpQOq+Uu0w57kdUkT4RlQKlwhT8T81arEsbV55KpzkpFsOZP7J1wdmhBw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/json-file-loader@8.0.0': - resolution: {integrity: sha512-ki6EF/mobBWJjAAC84xNrFMhNfnUFD6Y0rQMGXekrUgY0NdeYXHU0ZUgHzC9O5+55FslqUmAUHABePDHTyZsLg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/load@8.0.0': - resolution: {integrity: sha512-Cy874bQJH0FP2Az7ELPM49iDzOljQmK1PPH6IuxsWzLSTxwTqd8dXA09dcVZrI7/LsN26heTY2R8q2aiiv0GxQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/merge@9.0.0': - resolution: {integrity: sha512-J7/xqjkGTTwOJmaJQJ2C+VDBDOWJL3lKrHJN4yMaRLAJH3PosB7GiPRaSDZdErs0+F77sH2MKs2haMMkywzx7Q==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/optimize@2.0.0': - resolution: {integrity: sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/prisma-loader@8.0.1': - resolution: {integrity: sha512-bl6e5sAYe35Z6fEbgKXNrqRhXlCJYeWKBkarohgYA338/SD9eEhXtg3Cedj7fut3WyRLoQFpHzfiwxKs7XrgXg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/relay-operation-optimizer@7.0.0': - resolution: {integrity: sha512-UNlJi5y3JylhVWU4MBpL0Hun4Q7IoJwv9xYtmAz+CgRa066szzY7dcuPfxrA7cIGgG/Q6TVsKsYaiF4OHPs1Fw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/schema@10.0.0': - resolution: {integrity: sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/url-loader@8.0.0': - resolution: {integrity: sha512-rPc9oDzMnycvz+X+wrN3PLrhMBQkG4+sd8EzaFN6dypcssiefgWKToXtRKI8HHK68n2xEq1PyrOpkjHFJB+GwA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/utils@10.0.6': - resolution: {integrity: sha512-hZMjl/BbX10iagovakgf3IiqArx8TPsotq5pwBld37uIX1JiZoSbgbCIFol7u55bh32o6cfDEiiJgfAD5fbeyQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/wrap@10.0.1': - resolution: {integrity: sha512-Cw6hVrKGM2OKBXeuAGltgy4tzuqQE0Nt7t/uAqnuokSXZhMHXJUb124Bnvxc2gPZn5chfJSDafDe4Cp8ZAVJgg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-typed-document-node/core@3.2.0': - resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-yoga/logger@2.0.0': - resolution: {integrity: sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==} - engines: {node: '>=18.0.0'} - - '@graphql-yoga/subscription@5.0.0': - resolution: {integrity: sha512-Ri7sK8hmxd/kwaEa0YT8uqQUb2wOLsmBMxI90QDyf96lzOMJRgBuNYoEkU1pSgsgmW2glceZ96sRYfaXqwVxUw==} - engines: {node: '>=18.0.0'} - - '@graphql-yoga/typed-event-target@3.0.0': - resolution: {integrity: sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==} - engines: {node: '>=18.0.0'} - '@headlessui/react@2.2.0': resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} engines: {node: '>=10'} @@ -4205,17 +3775,6 @@ packages: resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==} engines: {node: '>= 10.0.0'} - '@peculiar/asn1-schema@2.3.6': - resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==} - - '@peculiar/json-schema@1.1.12': - resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} - engines: {node: '>=8.0.0'} - - '@peculiar/webcrypto@1.4.3': - resolution: {integrity: sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==} - engines: {node: '>=10.12.0'} - '@peggyjs/from-mem@1.2.1': resolution: {integrity: sha512-qh5zG8WKT36142/FqOYtpF0scRR3ZJ3H5XST1bJ/KV2FvyB5MvUB/tB9ZjihRe1iKjJD4PBOZczzwEx7hJtgMw==} engines: {node: '>=18'} @@ -4228,57 +3787,6 @@ packages: resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@pothos/core@3.41.1': - resolution: {integrity: sha512-K+TGTK2Q7rmLU9WaC1cSDiGZaU9M+gHNbCYBom2W1vHuEYDUAiihVHz9tXYsrYjFMJSK+wLJ7Xp2374bQa9x/w==} - peerDependencies: - graphql: '>=15.1.0' - - '@pothos/plugin-errors@3.11.1': - resolution: {integrity: sha512-ty+L1yfGartuuIMszGZyDQtzwpt/7ijVbnmGfOZdcQZOlk1P0MUr1iJ56Z5QhaHwZuwyVBF17gwP7ZroGs9htg==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - - '@pothos/plugin-prisma@3.65.2': - resolution: {integrity: sha512-Ahv2UfHhKINNwjWw7kO3esg7CFePg4xEUrvvSPqI6/Qt+XmvA0btXpN/lYknRoz93M/BuJLYoH15DslHCazhWA==} - hasBin: true - peerDependencies: - '@pothos/core': '*' - '@prisma/client': '*' - graphql: 16.8.1 - typescript: '>=4.7.2' - - '@pothos/plugin-relay@3.46.0': - resolution: {integrity: sha512-EOmyqAsMy/vWgitPWcQRDUvLBcZF5M/RrFxoFeI/VWnRpUJPr8XmHh28jb70G8Wzfg56btPpTY440doSKb60og==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - - '@pothos/plugin-scope-auth@3.22.0': - resolution: {integrity: sha512-iT+JT0uIvoVGuQeQ2Q/y+wwP27SQH+1Hs8Uz5rrtGOblsKGW0cRcJYuXMi5+XJFXOY3CPNMnv6aroetxZkEcEQ==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - - '@pothos/plugin-simple-objects@3.7.0': - resolution: {integrity: sha512-CgZJLaHLt1Q30j+XCiWV6qVJcae1ksiUFdi8kz4sEsOhCNfdVwz64s8rx7SSqsdmPbjb68dJIDKpq2Qg9VZb8g==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - - '@pothos/plugin-validation@3.10.1': - resolution: {integrity: sha512-FdYHzVSbrHZ4cDPOW/RB8Zsyx7qNuybAbC5DO3zVSsPPgY/FWLNWtQ6CaE+tkg5Je1RxpmO5od7Ig1Wt+akqnA==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - zod: '*' - - '@pothos/plugin-with-input@3.10.1': - resolution: {integrity: sha512-1Vz1EWMgNO/F37LHxkMr6V5w7lqiDiIvkonSZvOAbLUzOpwbrAgf36j2NrXFWahTN8Q6lPt4x9pNHnBeH18pAg==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} engines: {node: '>=16.13'} @@ -4288,9 +3796,6 @@ packages: prisma: optional: true - '@prisma/debug@5.15.0': - resolution: {integrity: sha512-QpEAOjieLPc/4sMny/WrWqtpIAmBYsgqwWlWwIctqZO0AbhQ9QcT6x2Ut3ojbDo/pFRCCA1Z1+xm2MUy7fAkZA==} - '@prisma/debug@5.22.0': resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} @@ -4303,9 +3808,6 @@ packages: '@prisma/fetch-engine@5.22.0': resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} - '@prisma/generator-helper@5.15.0': - resolution: {integrity: sha512-7pB3v57GU4Q/iBauGbvQQGenMJSu2ArQboge4Ca6bw0gA7nConfIHP48MdNIYCrBbNPcIVFmrNomyhqCb3IuWQ==} - '@prisma/get-platform@5.22.0': resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} @@ -5050,9 +4552,6 @@ packages: react: '>=17' react-dom: '>=17' - '@repeaterjs/repeater@3.0.4': - resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==} - '@rollup/pluginutils@5.1.0': resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} @@ -5812,15 +5311,9 @@ packages: '@types/js-cookie@2.2.7': resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} - '@types/js-yaml@4.0.5': - resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} - '@types/jsdom@20.0.1': resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} - '@types/json-stable-stringify@1.0.34': - resolution: {integrity: sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==} - '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -5899,18 +5392,12 @@ packages: '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} - '@types/react-relay@16.0.6': - resolution: {integrity: sha512-VTntVQJhlwQYNUlbNgGf8RYy7EtQPRZqsD/w2Si0ygZspJXuNlVdRkklWMFN99EMRhHDpqlNHD8i3wIs7QRz9g==} - '@types/react-transition-group@4.4.6': resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} - '@types/relay-runtime@14.1.23': - resolution: {integrity: sha512-tP2l6YLI2HJ11UzEB7j4IWeADyiPIKTehdeyHsyOzNBu7WvKsyf4kAZDmsB2NPaXp9Lud+KEJbRi/VW+jEDYCA==} - '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} @@ -5947,9 +5434,6 @@ packages: '@types/vscode@1.86.0': resolution: {integrity: sha512-DnIXf2ftWv+9LWOB5OJeIeaLigLHF7fdXF6atfc7X5g2w/wVZBgk0amP7b+ub5xAuW1q7qP5YcFvOcit/DtyCQ==} - '@types/ws@8.5.5': - resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} - '@types/yargs-parser@21.0.0': resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -6139,31 +5623,6 @@ packages: engines: {node: '>= 16'} hasBin: true - '@whatwg-node/events@0.0.3': - resolution: {integrity: sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA==} - - '@whatwg-node/events@0.1.1': - resolution: {integrity: sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==} - engines: {node: '>=16.0.0'} - - '@whatwg-node/fetch@0.8.8': - resolution: {integrity: sha512-CdcjGC2vdKhc13KKxgsc6/616BQ7ooDIgPeTuAiE8qfCnS0mGzcfCOoZXypQSz73nxI+GWc7ZReIAVhxoE1KCg==} - - '@whatwg-node/fetch@0.9.7': - resolution: {integrity: sha512-heClS5ctTmoEvVbFd+6ztX0SyQduI3/Q+77vtNApDU/+Mwajy6ugxaoDGgSzJUoQ37McSV09kcGCt1Jcc6z9lQ==} - engines: {node: '>=16.0.0'} - - '@whatwg-node/node-fetch@0.3.6': - resolution: {integrity: sha512-w9wKgDO4C95qnXZRwZTfCmLWqyRnooGjcIwG0wADWjw9/HN0p7dtvtgSvItZtUyNteEvgTrd8QojNEqV6DAGTA==} - - '@whatwg-node/node-fetch@0.4.6': - resolution: {integrity: sha512-9oLD57yW0WWfu4ZtEmybBrx1JfkO4TzDpD2zXlp2Ue3UJplifQRap+MbE0PxRlVWtR4KWsIRamhn7J44DkwoyA==} - engines: {node: '>=16.0.0'} - - '@whatwg-node/server@0.9.1': - resolution: {integrity: sha512-Zq0FWafIlJzYyyAL7UwzsEhqSMzvKci/XqIFKpC4V031PxKKPBmZSP1mXCew5eZFW8Z5Sj6bZNi2CIsHd062ug==} - engines: {node: '>=16.0.0'} - '@wogns3623/eslint-plugin-better-exhaustive-deps@1.1.0': resolution: {integrity: sha512-AkQzn7bSngpml8nqkJmXMTHfoO1sviqTfc6jaNaQKX1Rj6jHqRmm4hzn1ahN3oH4b7sTjMnzXfGjN/16XcFKxw==} peerDependencies: @@ -6373,16 +5832,9 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} - asn1js@3.0.5: - resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} - engines: {node: '>=12.0.0'} - assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} @@ -6396,10 +5848,6 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - astring@1.8.6: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true @@ -6413,10 +5861,6 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - auto-bind@4.0.0: - resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} - engines: {node: '>=8'} - auto-bind@5.0.1: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6468,9 +5912,6 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - babel-plugin-macros@2.8.0: - resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==} - babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -6490,22 +5931,11 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-relay@16.2.0: - resolution: {integrity: sha512-+3n7kSFH5MelySnO5MLXl2S+Bq4nAGcdWylXeqNXZODbzgYtqak194Z4u5KElLAHGhsyKIMTW7qazBPvMbxhFQ==} - - babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: - resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} - babel-preset-current-node-syntax@1.0.1: resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: '@babel/core': ^7.0.0 - babel-preset-fbjs@3.4.0: - resolution: {integrity: sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==} - peerDependencies: - '@babel/core': ^7.0.0 - babel-preset-jest@29.6.3: resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6608,11 +6038,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.23.3: - resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.24.2: resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -6666,25 +6091,10 @@ packages: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} - caller-callsite@2.0.0: - resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} - engines: {node: '>=4'} - - caller-path@2.0.0: - resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} - engines: {node: '>=4'} - - callsites@2.0.0: - resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} - engines: {node: '>=4'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camel-case@4.1.2: - resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -6717,9 +6127,6 @@ packages: caniuse-lite@1.0.30001680: resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} - capital-case@1.0.4: - resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -6743,12 +6150,6 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - change-case-all@1.0.15: - resolution: {integrity: sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==} - - change-case@4.1.2: - resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -6839,18 +6240,10 @@ packages: resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} engines: {node: 10.* || >= 12.*} - cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - cli-truncate@4.0.0: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} - cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -6937,9 +6330,6 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -6975,10 +6365,6 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} - common-tags@1.8.2: - resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} - engines: {node: '>=4.0.0'} - commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -7004,9 +6390,6 @@ packages: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} - constant-case@3.0.4: - resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -7048,27 +6431,10 @@ packages: cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} - cosmiconfig@5.2.1: - resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} - engines: {node: '>=4'} - - cosmiconfig@6.0.0: - resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} - engines: {node: '>=8'} - cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} - cosmiconfig@8.3.6: - resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - create-ecdh@4.0.4: resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} @@ -7089,9 +6455,6 @@ packages: crelt@1.0.5: resolution: {integrity: sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==} - cross-fetch@3.1.8: - resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} - cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -7330,9 +6693,6 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - dataloader@2.2.2: - resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==} - date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -7342,9 +6702,6 @@ packages: dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} - debounce@1.2.1: - resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} - debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -7596,9 +6953,6 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - dot-case@3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - dotenv-cli@7.4.2: resolution: {integrity: sha512-SbUj8l61zIbzyhIbg0FwPJq6+wjbzdn9oEtozQpZ6kW2ihCcapKVZj49oCT3oPM+mgQm+itgvUQcG5szxVrZTA==} hasBin: true @@ -7611,10 +6965,6 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} - dset@3.1.2: - resolution: {integrity: sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==} - engines: {node: '>=4'} - duplexify@3.7.1: resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} @@ -7635,9 +6985,6 @@ packages: electron-to-chromium@1.4.752: resolution: {integrity: sha512-P3QJreYI/AUTcfBVrC4zy9KvnZWekViThgQMX/VpJ+IsOBbcX5JFpORM4qWapwWQ+agb2nYAOyn/4PMXOk0m2Q==} - electron-to-chromium@1.5.13: - resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==} - electron-to-chromium@1.5.51: resolution: {integrity: sha512-kKeWV57KSS8jH4alKt/jKnvHPmJgBxXzGUSbMd4eQF+iOsVPl7bz2KUmu6eo80eMP8wVioTfTyTzdMgM15WXNg==} @@ -8002,17 +7349,10 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} - extract-files@11.0.0: - resolution: {integrity: sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==} - engines: {node: ^12.20 || >= 14.13} - fast-check@3.19.0: resolution: {integrity: sha512-CO2JX/8/PT9bDGO1iXa5h5ey1skaKI1dvecERyhH4pp3PGjwd3KIjMAXEg79Ps9nclsdt4oPbfqiAnLU0EwrAQ==} engines: {node: '>=8.0.0'} - fast-decode-uri-component@1.0.1: - resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -8029,9 +7369,6 @@ packages: fast-loops@1.1.3: resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==} - fast-querystring@1.1.1: - resolution: {integrity: sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==} - fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} @@ -8054,12 +7391,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fbjs-css-vars@1.0.2: - resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} - - fbjs@3.0.5: - resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} - fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -8074,10 +7405,6 @@ packages: fetch-retry@5.0.6: resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} - figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - figures@5.0.0: resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} engines: {node: '>=14'} @@ -8405,47 +7732,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql-config@5.0.2: - resolution: {integrity: sha512-7TPxOrlbiG0JplSZYCyxn2XQtqVhXomEjXUmWJVSS5ET1nPhOJSsIb/WTwqWhcYX6G0RlHXSj9PLtGTKmxLNGg==} - engines: {node: '>= 16.0.0'} - peerDependencies: - cosmiconfig-toml-loader: ^1.0.0 - graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - peerDependenciesMeta: - cosmiconfig-toml-loader: - optional: true - - graphql-request@6.1.0: - resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} - peerDependencies: - graphql: 14 - 16 - - graphql-tag@2.12.6: - resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} - engines: {node: '>=10'} - peerDependencies: - graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - graphql-ws@5.14.0: - resolution: {integrity: sha512-itrUTQZP/TgswR4GSSYuwWUzrE/w5GhbwM2GX3ic2U7aw33jgEsayfIlvaj7/GcIvZgNMzsPTrE5hqPuFUiE5g==} - engines: {node: '>=10'} - peerDependencies: - graphql: '>=0.11 <=16' - - graphql-yoga@5.1.1: - resolution: {integrity: sha512-oak5nVKTHpqJgpA1aT3cJPOlCidrW7l6nbc5L6w07VdFul16ielGI2ZnQDAXO+qQih09/4WspD5x0SsSZH+hkg==} - engines: {node: '>=18.0.0'} - peerDependencies: - graphql: ^15.2.0 || ^16.0.0 - - graphql@15.3.0: - resolution: {integrity: sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==} - engines: {node: '>= 10.x'} - - graphql@16.8.1: - resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} @@ -8567,9 +7853,6 @@ packages: hastscript@8.0.0: resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} - header-case@2.0.4: - resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} - heap@0.2.7: resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} @@ -8626,10 +7909,6 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} - http-proxy-agent@7.0.0: - resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} - engines: {node: '>= 14'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -8638,10 +7917,6 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - https-proxy-agent@7.0.2: - resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} - engines: {node: '>= 14'} - https-proxy-agent@7.0.4: resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} engines: {node: '>= 14'} @@ -8700,25 +7975,13 @@ packages: engines: {node: '>=16.x'} hasBin: true - immutable@3.7.6: - resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} - engines: {node: '>=0.8.0'} - immutable@4.3.6: resolution: {integrity: sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==} - import-fresh@2.0.0: - resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} - engines: {node: '>=4'} - import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} - import-from@4.0.0: - resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} - engines: {node: '>=12.2'} - import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -8775,10 +8038,6 @@ packages: inline-style-prefixer@7.0.0: resolution: {integrity: sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==} - inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} - internal-slot@1.0.5: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} @@ -8801,10 +8060,6 @@ packages: resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-absolute@1.0.0: - resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} - engines: {node: '>=0.10.0'} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -8860,10 +8115,6 @@ packages: is-deflate@1.0.0: resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} - is-directory@0.3.1: - resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} - engines: {node: '>=0.10.0'} - is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -8930,9 +8181,6 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - is-lower-case@2.0.2: - resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} - is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} @@ -8992,10 +8240,6 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} - is-relative@1.0.0: - resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} - engines: {node: '>=0.10.0'} - is-set@2.0.2: resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} @@ -9030,10 +8274,6 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} - is-unc-path@1.0.0: - resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} - engines: {node: '>=0.10.0'} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -9042,9 +8282,6 @@ packages: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} - is-upper-case@2.0.2: - resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} - is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} @@ -9092,11 +8329,6 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - isomorphic-ws@5.0.0: - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} - peerDependencies: - ws: '*' - istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -9274,17 +8506,10 @@ packages: node-notifier: optional: true - jiti@1.20.0: - resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==} - hasBin: true - jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true - jose@4.15.5: - resolution: {integrity: sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==} - jose@5.9.6: resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} @@ -9346,9 +8571,6 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -9362,13 +8584,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stable-stringify@1.0.2: - resolution: {integrity: sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==} - - json-to-pretty-yaml@1.2.2: - resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} - engines: {node: '>= 0.2.0'} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -9387,9 +8602,6 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - jsonify@0.0.1: - resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} - jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -9503,15 +8715,6 @@ packages: linkify-it@3.0.3: resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} - listr2@4.0.5: - resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} - engines: {node: '>=12'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true - lite-emit@2.3.0: resolution: {integrity: sha512-QMPrnwPho7lfkzZUN3a0RJ/oiwpt464eXf6aVh1HGOYh+s7Utu78q3FcFbW59c8TNWWQaz9flKN1cEb8dmxD+g==} @@ -9570,9 +8773,6 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -9583,10 +8783,6 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-update@4.0.0: - resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} - engines: {node: '>=10'} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -9597,16 +8793,6 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - lower-case-first@2.0.2: - resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} - - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - - lru-cache@10.0.0: - resolution: {integrity: sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==} - engines: {node: 14 || >=16.14} - lru-cache@10.2.2: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} @@ -9653,10 +8839,6 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - map-cache@0.2.2: - resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} - engines: {node: '>=0.10.0'} - map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -9796,15 +8978,6 @@ packages: mermaid@10.9.0: resolution: {integrity: sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==} - meros@1.3.0: - resolution: {integrity: sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==} - engines: {node: '>=13'} - peerDependencies: - '@types/node': '>=13' - peerDependenciesMeta: - '@types/node': - optional: true - methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -10033,10 +9206,6 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@4.2.3: - resolution: {integrity: sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng==} - engines: {node: '>=10'} - minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -10150,6 +9319,27 @@ packages: nodemailer: optional: true + next-safe-action@7.9.9: + resolution: {integrity: sha512-wFKKCgfHNsObfbDrbOQV8WAE6RnVx7dwmuUazqdNaTL3ZdDzUlRTnIIVI36qSjmgA3zwwxj3nvfxgK9d0fWr5w==} + engines: {node: '>=18.17'} + peerDependencies: + '@sinclair/typebox': '>= 0.33.3' + next: '>= 14.0.0' + react: '>= 18.2.0' + react-dom: '>= 18.2.0' + valibot: '>= 0.36.0' + yup: '>= 1.0.0' + zod: '>= 3.0.0' + peerDependenciesMeta: + '@sinclair/typebox': + optional: true + valibot: + optional: true + yup: + optional: true + zod: + optional: true + next-themes@0.3.0: resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} peerDependencies: @@ -10219,9 +9409,6 @@ packages: sass: optional: true - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - node-abi@3.47.0: resolution: {integrity: sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==} engines: {node: '>=10'} @@ -10289,10 +9476,6 @@ packages: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} - normalize-path@2.1.1: - resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} - engines: {node: '>=0.10.0'} - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -10328,9 +9511,6 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nullthrows@1.1.1: - resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} - nwsapi@2.2.10: resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==} @@ -10510,9 +9690,6 @@ packages: pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - param-case@3.0.4: - resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -10523,14 +9700,6 @@ packages: parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} - parse-filepath@1.0.2: - resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} - engines: {node: '>=0.8'} - - parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} - parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -10551,9 +9720,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - pascal-case@3.1.2: - resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -10561,9 +9727,6 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-case@3.0.4: - resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} - path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -10587,14 +9750,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-root-regex@0.1.2: - resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} - engines: {node: '>=0.10.0'} - - path-root@0.1.1: - resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} - engines: {node: '>=0.10.0'} - path-scurry@1.10.2: resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} engines: {node: '>=16 || 14 >=14.17'} @@ -10898,9 +10053,6 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} - promise@7.3.1: - resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} - prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -10955,13 +10107,6 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - pvtsutils@1.3.5: - resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} - - pvutils@1.1.3: - resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} - engines: {node: '>=6.0.0'} - qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -11024,6 +10169,11 @@ packages: peerDependencies: react: ^18.3.1 + react-dom@19.0.0-rc-66855b96-20241106: + resolution: {integrity: sha512-D25vdaytZ1wFIRiwNU98NPQ/upS2P8Co4/oNoa02PzHbh8deWdepjm5qwZM/46OdSiGv4WSWwxP55RO9obqJEQ==} + peerDependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom@19.0.0-rc-cae764ce-20241025: resolution: {integrity: sha512-e3CVe2+ojMe4dz8E/WsV9bkRj+lZt5ms+rhTFHEqIAHv4/PDdXa7P4uJXNhfik+ZYF4Wg5wCDVP4l7cgaudCpg==} peerDependencies: @@ -11046,11 +10196,11 @@ packages: peerDependencies: react: '>=16.13.1' - react-hook-form@7.50.0: - resolution: {integrity: sha512-AOhuzM3RdP09ZCnq+Z0yvKGHK25yiOX5phwxjV9L7U6HMla10ezkBnvQ+Pk4GTuDfsC5P2zza3k8mawFwFLVuQ==} - engines: {node: '>=12.22.0'} + react-hook-form@7.53.2: + resolution: {integrity: sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==} + engines: {node: '>=18.0.0'} peerDependencies: - react: ^16.8.0 || ^17 || ^18 + react: ^16.8.0 || ^17 || ^18 || ^19 react-icons@5.2.1: resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==} @@ -11098,11 +10248,6 @@ packages: peerDependencies: react: ^18.3.1 - react-relay@16.2.0: - resolution: {integrity: sha512-f/HtC4whyYmK6/WUeOVakXRoBkV+JEgoSeBHXfIC2U6AuH14NrKXnFicX65LksfzgD1OUfYF6IqGQ4MvO52lTQ==} - peerDependencies: - react: ^16.9.0 || ^17 || ^18 - react-remove-scroll-bar@2.3.6: resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} @@ -11194,6 +10339,10 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + react@19.0.0-rc-66855b96-20241106: + resolution: {integrity: sha512-klH7xkT71SxRCx4hb1hly5FJB21Hz0ACyxbXYAECEqssUjtJeFUAaI2U1DgJAzkGEnvEm3DkxuBchMC/9K4ipg==} + engines: {node: '>=0.10.0'} + react@19.0.0-rc-cae764ce-20241025: resolution: {integrity: sha512-5wV/3MJc6Ws4l4ZF95yaQKaMV8aWVlIBKOdPA4Kere7CfdJ0NMIuKt9j9v0U4ZTmCi4ubAdN+KL4gGdfTEIpuw==} engines: {node: '>=0.10.0'} @@ -11332,16 +10481,6 @@ packages: rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} - relay-compiler@16.2.0: - resolution: {integrity: sha512-KuyzUBKL9PZRNtIZWNlWEOl7OliUxaGJ2d+3mkiWEiGCEuGnNTxqEg4kJyL341aIGZC4gSqEpfvRTcMqnSM4qQ==} - hasBin: true - - relay-runtime@12.0.0: - resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} - - relay-runtime@16.2.0: - resolution: {integrity: sha512-SrIyYItH1EZUj37NI8nZALasuq7mNyFrrSNgMefhgxNZxTVnr1KCp43LaxUfZqhsWbw4Y00JSGDRQXlcv4STHQ==} - remark-breaks@4.0.0: resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} @@ -11378,15 +10517,6 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - remedial@1.0.8: - resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} - - remove-trailing-separator@1.1.0: - resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} - - remove-trailing-spaces@1.0.8: - resolution: {integrity: sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -11404,10 +10534,6 @@ packages: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} - resolve-from@3.0.0: - resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} - engines: {node: '>=4'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -11447,9 +10573,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -11488,19 +10611,12 @@ packages: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} - run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -11531,6 +10647,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0-rc-66855b96-20241106: + resolution: {integrity: sha512-HQXp/Mnp/MMRSXMQF7urNFla+gmtXW/Gr1KliuR0iboTit4KvZRY8KYaq5ccCTAOJiUqQh2rE2F3wgUekmgdlA==} + scheduler@0.25.0-rc-cae764ce-20241025: resolution: {integrity: sha512-kiDqIcp0nrZ8RW65wMujBEs7eDNfd49hcfjDmscxWIsnDTz9NRQrTAChv/tYRYCUNk7qPM36SQOja2HcRuee0A==} @@ -11541,9 +10660,6 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - scuid@1.1.0: - resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} - section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -11579,9 +10695,6 @@ packages: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} - sentence-case@3.0.4: - resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} - serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -11611,9 +10724,6 @@ packages: resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} engines: {node: '>=6.9'} - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -11645,9 +10755,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - shiki@0.14.7: resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} @@ -11684,9 +10791,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - signedsource@1.0.0: - resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} - simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -11715,14 +10819,6 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - - slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -11736,9 +10832,6 @@ packages: engines: {node: '>=6'} hasBin: true - snake-case@3.0.4: - resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - source-map-generator@0.8.0: resolution: {integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==} engines: {node: '>= 10'} @@ -11791,9 +10884,6 @@ packages: spdx-license-ids@3.0.12: resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} - sponge-case@1.0.1: - resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -11841,9 +10931,6 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - string-env-interpolation@1.0.1: - resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -11985,9 +11072,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swap-case@2.0.2: - resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -12081,9 +11165,6 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} @@ -12098,9 +11179,6 @@ packages: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} - title-case@3.0.3: - resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} - titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -12193,9 +11271,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-log@2.2.5: - resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} - ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -12366,9 +11441,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-parser-js@1.0.37: - resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} - uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} @@ -12383,10 +11455,6 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - unc-path-regex@0.1.2: - resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} - engines: {node: '>=0.10.0'} - undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} @@ -12493,10 +11561,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unixify@1.0.0: - resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} - engines: {node: '>=0.10.0'} - unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -12515,24 +11579,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-browserslist-db@1.1.0: - resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - upper-case-first@2.0.2: - resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} - - upper-case@2.0.2: - resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} - uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -12545,12 +11597,6 @@ packages: urlgrey@1.0.0: resolution: {integrity: sha512-hJfIzMPJmI9IlLkby8QrsCykQ+SXDeO2W5Q9QTW3QpqZVTx4a/K7p8/5q+/isD8vsbVaFgql/gvAoQCRQ2Cb5w==} - urlpattern-polyfill@8.0.2: - resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} - - urlpattern-polyfill@9.0.0: - resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==} - use-callback-ref@1.3.2: resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} engines: {node: '>=10'} @@ -12636,10 +11682,6 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - value-or-promise@1.0.12: - resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} - engines: {node: '>=12'} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -12779,10 +11821,6 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-streams-polyfill@3.2.1: - resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} - engines: {node: '>= 8'} - web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -12790,9 +11828,6 @@ packages: web-worker@1.2.0: resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==} - webcrypto-core@1.7.7: - resolution: {integrity: sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -12906,30 +11941,6 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@8.14.1: - resolution: {integrity: sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.17.0: resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} engines: {node: '>=10.0.0'} @@ -13009,9 +12020,6 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml-ast-parser@0.0.43: - resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} - yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -13208,36 +12216,6 @@ snapshots: transitivePeerDependencies: - encoding - '@ardatan/relay-compiler@12.0.0(graphql@16.8.1)': - dependencies: - '@babel/core': 7.26.0 - '@babel/generator': 7.24.1 - '@babel/parser': 7.26.2 - '@babel/runtime': 7.26.0 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 - babel-preset-fbjs: 3.4.0(@babel/core@7.26.0) - chalk: 4.1.2 - fb-watchman: 2.0.2 - fbjs: 3.0.5 - glob: 7.2.3 - graphql: 16.8.1 - immutable: 3.7.6 - invariant: 2.2.4 - nullthrows: 1.1.1 - relay-runtime: 12.0.0 - signedsource: 1.0.0 - yargs: 15.4.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@ardatan/sync-fetch@0.0.1': - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - '@auth/core@0.37.2(nodemailer@6.9.13)': dependencies: '@panva/hkdf': 1.2.1 @@ -13377,8 +12355,6 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.0.1 - '@babel/compat-data@7.25.4': {} - '@babel/compat-data@7.26.2': {} '@babel/core@7.26.0': @@ -13401,20 +12377,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.23.6': - dependencies: - '@babel/types': 7.24.0 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - - '@babel/generator@7.24.1': - dependencies: - '@babel/types': 7.25.6 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - '@babel/generator@7.25.6': dependencies: '@babel/types': 7.25.6 @@ -13449,14 +12411,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-compilation-targets@7.25.2': - dependencies: - '@babel/compat-data': 7.25.4 - '@babel/helper-validator-option': 7.24.8 - browserslist: 4.23.3 - lru-cache: 5.1.1 - semver: 6.3.1 - '@babel/helper-compilation-targets@7.25.9': dependencies: '@babel/compat-data': 7.26.2 @@ -13811,23 +12765,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.26.0) - '@babel/helper-plugin-utils': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.26.0)': - dependencies: - '@babel/compat-data': 7.25.4 - '@babel/core': 7.26.0 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -13852,12 +12789,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 @@ -13938,11 +12870,6 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.26.0) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -13966,21 +12893,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14010,20 +12927,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.26.0) - '@babel/helper-split-export-declaration': 7.24.7 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14036,23 +12939,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/template': 7.25.0 - '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/template': 7.25.9 - '@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14099,14 +12991,6 @@ snapshots: '@babel/helper-plugin-utils': 7.24.7 '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.26.0) - '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14115,13 +12999,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14136,11 +13013,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-literals@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14151,11 +13023,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14246,14 +13113,6 @@ snapshots: '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.26.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14284,11 +13143,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14319,11 +13173,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14375,24 +13224,11 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-spread@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14406,11 +13242,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14611,12 +13442,6 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.23.9': - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/parser': 7.26.2 - '@babel/types': 7.24.0 - '@babel/template@7.24.0': dependencies: '@babel/code-frame': 7.24.2 @@ -14635,21 +13460,6 @@ snapshots: '@babel/parser': 7.26.2 '@babel/types': 7.26.0 - '@babel/traverse@7.24.1': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.25.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.26.2 - '@babel/types': 7.25.6 - debug: 4.3.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.24.5': dependencies: '@babel/code-frame': 7.24.7 @@ -15120,6 +13930,20 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@emotion/react@11.11.0(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.23.9 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.0.0-rc-66855b96-20241106) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + hoist-non-react-statics: 3.3.2 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@emotion/serialize@1.1.2': dependencies: '@emotion/hash': 0.9.1 @@ -15136,19 +13960,14 @@ snapshots: dependencies: react: 18.3.1 + '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + '@emotion/utils@1.2.1': {} '@emotion/weak-memoize@0.3.1': {} - '@envelop/core@5.0.0': - dependencies: - '@envelop/types': 5.0.0 - tslib: 2.6.2 - - '@envelop/types@5.0.0': - dependencies: - tslib: 2.8.1 - '@esbuild-plugins/node-resolve@0.2.2(esbuild@0.21.5)': dependencies: '@types/resolve': 1.20.6 @@ -15494,485 +14313,54 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react-dom@2.1.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@floating-ui/dom': 1.6.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/react-dom@2.1.0(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@floating-ui/dom': 1.6.1 react: 19.0.0-rc-cae764ce-20241025 react-dom: 19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025) - '@floating-ui/react@0.24.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - aria-hidden: 1.2.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tabbable: 6.2.0 - - '@floating-ui/react@0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@floating-ui/react-dom': 2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@floating-ui/utils': 0.2.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tabbable: 6.2.0 - - '@floating-ui/utils@0.2.1': {} - - '@formatjs/intl-localematcher@0.5.7': - dependencies: - tslib: 2.8.1 - - '@fumadocs/content-collections@1.1.5(@content-collections/core@0.7.3(typescript@5.6.3))(@content-collections/mdx@0.2.0(@content-collections/core@0.7.3(typescript@5.6.3))(acorn@8.11.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025))(fumadocs-core@14.0.2(@types/react@18.3.3))': - dependencies: - '@content-collections/core': 0.7.3(typescript@5.6.3) - '@content-collections/mdx': 0.2.0(@content-collections/core@0.7.3(typescript@5.6.3))(acorn@8.11.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025) - fumadocs-core: 14.0.2(@types/react@18.3.3) - - '@graphql-codegen/add@5.0.2(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - - '@graphql-codegen/cli@5.0.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(enquirer@2.3.6)(graphql@16.8.1)(typescript@5.6.3)': - dependencies: - '@babel/generator': 7.23.6 - '@babel/template': 7.23.9 - '@babel/types': 7.23.9 - '@graphql-codegen/client-preset': 4.2.5(graphql@16.8.1) - '@graphql-codegen/core': 4.0.2(graphql@16.8.1) - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-tools/apollo-engine-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/code-file-loader': 8.0.2(graphql@16.8.1) - '@graphql-tools/git-loader': 8.0.2(graphql@16.8.1) - '@graphql-tools/github-loader': 8.0.0(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/json-file-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/load': 8.0.0(graphql@16.8.1) - '@graphql-tools/prisma-loader': 8.0.1(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/url-loader': 8.0.0(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@whatwg-node/fetch': 0.8.8 - chalk: 4.1.2 - cosmiconfig: 8.3.6(typescript@5.6.3) - debounce: 1.2.1 - detect-indent: 6.1.0 - graphql: 16.8.1 - graphql-config: 5.0.2(@types/node@20.12.7)(graphql@16.8.1)(typescript@5.6.3) - inquirer: 8.2.6 - is-glob: 4.0.3 - jiti: 1.20.0 - json-to-pretty-yaml: 1.2.2 - listr2: 4.0.5(enquirer@2.3.6) - log-symbols: 4.1.0 - micromatch: 4.0.5 - shell-quote: 1.8.1 - string-env-interpolation: 1.0.1 - ts-log: 2.2.5 - tslib: 2.6.2 - yaml: 2.3.4 - yargs: 17.7.2 - optionalDependencies: - '@parcel/watcher': 2.4.1 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - cosmiconfig-toml-loader - - encoding - - enquirer - - supports-color - - typescript - - utf-8-validate - - '@graphql-codegen/client-preset@4.2.5(graphql@16.8.1)': - dependencies: - '@babel/helper-plugin-utils': 7.24.5 - '@babel/template': 7.24.0 - '@graphql-codegen/add': 5.0.2(graphql@16.8.1) - '@graphql-codegen/gql-tag-operations': 4.0.6(graphql@16.8.1) - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/typed-document-node': 5.0.6(graphql@16.8.1) - '@graphql-codegen/typescript': 4.0.6(graphql@16.8.1) - '@graphql-codegen/typescript-operations': 4.2.0(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - '@graphql-tools/documents': 1.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/core@4.0.2(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - - '@graphql-codegen/gql-tag-operations@4.0.6(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - auto-bind: 4.0.0 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/plugin-helpers@5.0.3(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - change-case-all: 1.0.15 - common-tags: 1.8.2 - graphql: 16.8.1 - import-from: 4.0.0 - lodash: 4.17.21 - tslib: 2.6.2 - - '@graphql-codegen/schema-ast@4.0.2(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - - '@graphql-codegen/typed-document-node@5.0.6(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - auto-bind: 4.0.0 - change-case-all: 1.0.15 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/typescript-operations@4.2.0(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/typescript': 4.0.6(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - auto-bind: 4.0.0 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/typescript@4.0.6(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/schema-ast': 4.0.2(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - auto-bind: 4.0.0 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/visitor-plugin-common@5.1.0(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-tools/optimize': 2.0.0(graphql@16.8.1) - '@graphql-tools/relay-operation-optimizer': 7.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - auto-bind: 4.0.0 - change-case-all: 1.0.15 - dependency-graph: 0.11.0 - graphql: 16.8.1 - graphql-tag: 2.12.6(graphql@16.8.1) - parse-filepath: 1.0.2 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-tools/apollo-engine-loader@8.0.0(graphql@16.8.1)': - dependencies: - '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@whatwg-node/fetch': 0.9.7 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - '@graphql-tools/batch-execute@9.0.2(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - dataloader: 2.2.2 - graphql: 16.8.1 - tslib: 2.8.1 - value-or-promise: 1.0.12 - - '@graphql-tools/code-file-loader@8.0.2(graphql@16.8.1)': - dependencies: - '@graphql-tools/graphql-tag-pluck': 8.0.2(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - globby: 11.1.0 - graphql: 16.8.1 - tslib: 2.6.2 - unixify: 1.0.0 - transitivePeerDependencies: - - supports-color - - '@graphql-tools/delegate@10.0.3(graphql@16.8.1)': - dependencies: - '@graphql-tools/batch-execute': 9.0.2(graphql@16.8.1) - '@graphql-tools/executor': 1.1.0(graphql@16.8.1) - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - dataloader: 2.2.2 - graphql: 16.8.1 - tslib: 2.8.1 - - '@graphql-tools/documents@1.0.0(graphql@16.8.1)': - dependencies: - graphql: 16.8.1 - lodash.sortby: 4.7.0 - tslib: 2.6.2 - - '@graphql-tools/executor-graphql-ws@1.1.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@types/ws': 8.5.5 - graphql: 16.8.1 - graphql-ws: 5.14.0(graphql@16.8.1) - isomorphic-ws: 5.0.0(ws@8.16.0) - tslib: 2.8.1 - ws: 8.16.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@graphql-tools/executor-http@1.0.2(@types/node@20.12.7)(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@repeaterjs/repeater': 3.0.4 - '@whatwg-node/fetch': 0.9.7 - extract-files: 11.0.0 - graphql: 16.8.1 - meros: 1.3.0(@types/node@20.12.7) - tslib: 2.8.1 - value-or-promise: 1.0.12 - transitivePeerDependencies: - - '@types/node' - - '@graphql-tools/executor-legacy-ws@1.0.3(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@types/ws': 8.5.5 - graphql: 16.8.1 - isomorphic-ws: 5.0.0(ws@8.14.1) - tslib: 2.8.1 - ws: 8.14.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@graphql-tools/executor@1.1.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - '@repeaterjs/repeater': 3.0.4 - graphql: 16.8.1 - tslib: 2.6.2 - value-or-promise: 1.0.12 - - '@graphql-tools/git-loader@8.0.2(graphql@16.8.1)': - dependencies: - '@graphql-tools/graphql-tag-pluck': 8.0.2(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - is-glob: 4.0.3 - micromatch: 4.0.5 - tslib: 2.6.2 - unixify: 1.0.0 - transitivePeerDependencies: - - supports-color - - '@graphql-tools/github-loader@8.0.0(@types/node@20.12.7)(graphql@16.8.1)': - dependencies: - '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/executor-http': 1.0.2(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/graphql-tag-pluck': 8.0.2(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@whatwg-node/fetch': 0.9.7 - graphql: 16.8.1 - tslib: 2.6.2 - value-or-promise: 1.0.12 - transitivePeerDependencies: - - '@types/node' - - encoding - - supports-color - - '@graphql-tools/graphql-file-loader@8.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/import': 7.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - globby: 11.1.0 - graphql: 16.8.1 - tslib: 2.6.2 - unixify: 1.0.0 - - '@graphql-tools/graphql-tag-pluck@8.0.2(graphql@16.8.1)': - dependencies: - '@babel/core': 7.26.0 - '@babel/parser': 7.26.2 - '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.26.0) - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@graphql-tools/import@7.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - resolve-from: 5.0.0 - tslib: 2.8.1 - - '@graphql-tools/json-file-loader@8.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - globby: 11.1.0 - graphql: 16.8.1 - tslib: 2.6.2 - unixify: 1.0.0 - - '@graphql-tools/load@8.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - p-limit: 3.1.0 - tslib: 2.6.2 - - '@graphql-tools/merge@9.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.8.1 - - '@graphql-tools/optimize@2.0.0(graphql@16.8.1)': - dependencies: - graphql: 16.8.1 - tslib: 2.8.1 - - '@graphql-tools/prisma-loader@8.0.1(@types/node@20.12.7)(graphql@16.8.1)': - dependencies: - '@graphql-tools/url-loader': 8.0.0(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@types/js-yaml': 4.0.5 - '@types/json-stable-stringify': 1.0.34 - '@whatwg-node/fetch': 0.9.7 - chalk: 4.1.2 - debug: 4.3.6 - dotenv: 16.4.5 - graphql: 16.8.1 - graphql-request: 6.1.0(graphql@16.8.1) - http-proxy-agent: 7.0.0 - https-proxy-agent: 7.0.2 - jose: 4.15.5 - js-yaml: 4.1.0 - json-stable-stringify: 1.0.2 - lodash: 4.17.21 - scuid: 1.1.0 - tslib: 2.6.2 - yaml-ast-parser: 0.0.43 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - supports-color - - utf-8-validate - - '@graphql-tools/relay-operation-optimizer@7.0.0(graphql@16.8.1)': - dependencies: - '@ardatan/relay-compiler': 12.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.8.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-tools/schema@10.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/merge': 9.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - value-or-promise: 1.0.12 - - '@graphql-tools/url-loader@8.0.0(@types/node@20.12.7)(graphql@16.8.1)': - dependencies: - '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/delegate': 10.0.3(graphql@16.8.1) - '@graphql-tools/executor-graphql-ws': 1.1.0(graphql@16.8.1) - '@graphql-tools/executor-http': 1.0.2(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/executor-legacy-ws': 1.0.3(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@graphql-tools/wrap': 10.0.1(graphql@16.8.1) - '@types/ws': 8.5.5 - '@whatwg-node/fetch': 0.9.7 - graphql: 16.8.1 - isomorphic-ws: 5.0.0(ws@8.16.0) - tslib: 2.6.2 - value-or-promise: 1.0.12 - ws: 8.16.0 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - utf-8-validate - - '@graphql-tools/utils@10.0.6(graphql@16.8.1)': - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - dset: 3.1.2 - graphql: 16.8.1 - tslib: 2.6.2 - - '@graphql-tools/wrap@10.0.1(graphql@16.8.1)': + '@floating-ui/react@0.24.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphql-tools/delegate': 10.0.3(graphql@16.8.1) - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.8.1 - value-or-promise: 1.0.12 + '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 - '@graphql-typed-document-node/core@3.2.0(graphql@16.8.1)': + '@floating-ui/react@0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - graphql: 16.8.1 + '@floating-ui/react-dom': 2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 - '@graphql-yoga/logger@2.0.0': + '@floating-ui/react@0.26.16(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - tslib: 2.6.2 + '@floating-ui/react-dom': 2.1.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/utils': 0.2.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + tabbable: 6.2.0 - '@graphql-yoga/subscription@5.0.0': - dependencies: - '@graphql-yoga/typed-event-target': 3.0.0 - '@repeaterjs/repeater': 3.0.4 - '@whatwg-node/events': 0.1.1 - tslib: 2.6.2 + '@floating-ui/utils@0.2.1': {} - '@graphql-yoga/typed-event-target@3.0.0': + '@formatjs/intl-localematcher@0.5.7': dependencies: - '@repeaterjs/repeater': 3.0.4 tslib: 2.8.1 + '@fumadocs/content-collections@1.1.5(@content-collections/core@0.7.3(typescript@5.6.3))(@content-collections/mdx@0.2.0(@content-collections/core@0.7.3(typescript@5.6.3))(acorn@8.11.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025))(fumadocs-core@14.0.2(@types/react@18.3.3))': + dependencies: + '@content-collections/core': 0.7.3(typescript@5.6.3) + '@content-collections/mdx': 0.2.0(@content-collections/core@0.7.3(typescript@5.6.3))(acorn@8.11.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025) + fumadocs-core: 14.0.2(@types/react@18.3.3) + '@headlessui/react@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15982,13 +14370,30 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@headlessui/react@2.2.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@floating-ui/react': 0.26.16(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@react-aria/focus': 3.17.1(react@19.0.0-rc-66855b96-20241106) + '@react-aria/interactions': 3.21.3(react@19.0.0-rc-66855b96-20241106) + '@tanstack/react-virtual': 3.10.8(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@heroicons/react@1.0.6(react@18.3.1)': dependencies: react: 18.3.1 - '@hookform/resolvers@3.3.4(react-hook-form@7.50.0(react@18.3.1))': + '@heroicons/react@1.0.6(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + + '@hookform/resolvers@3.3.4(react-hook-form@7.53.2(react@18.3.1))': + dependencies: + react-hook-form: 7.53.2(react@18.3.1) + + '@hookform/resolvers@3.3.4(react-hook-form@7.53.2(react@19.0.0-rc-66855b96-20241106))': dependencies: - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@19.0.0-rc-66855b96-20241106) '@humanwhocodes/config-array@0.11.14': dependencies: @@ -16705,24 +15110,6 @@ snapshots: '@parcel/watcher-win32-ia32': 2.4.1 '@parcel/watcher-win32-x64': 2.4.1 - '@peculiar/asn1-schema@2.3.6': - dependencies: - asn1js: 3.0.5 - pvtsutils: 1.3.5 - tslib: 2.8.1 - - '@peculiar/json-schema@1.1.12': - dependencies: - tslib: 2.8.1 - - '@peculiar/webcrypto@1.4.3': - dependencies: - '@peculiar/asn1-schema': 2.3.6 - '@peculiar/json-schema': 1.1.12 - pvtsutils: 1.3.5 - tslib: 2.8.1 - webcrypto-core: 1.7.7 - '@peggyjs/from-mem@1.2.1': dependencies: semver: 7.6.0 @@ -16739,55 +15126,10 @@ snapshots: tiny-glob: 0.2.9 tslib: 2.8.1 - '@pothos/core@3.41.1(graphql@16.8.1)': - dependencies: - graphql: 16.8.1 - - '@pothos/plugin-errors@3.11.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - - '@pothos/plugin-prisma@3.65.2(@pothos/core@3.41.1(graphql@16.8.1))(@prisma/client@5.22.0(prisma@5.22.0))(graphql@16.8.1)(typescript@5.6.3)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - '@prisma/client': 5.22.0(prisma@5.22.0) - '@prisma/generator-helper': 5.15.0 - graphql: 16.8.1 - typescript: 5.6.3 - - '@pothos/plugin-relay@3.46.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - - '@pothos/plugin-scope-auth@3.22.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - - '@pothos/plugin-simple-objects@3.7.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - - '@pothos/plugin-validation@3.10.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)(zod@3.23.8)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - zod: 3.23.8 - - '@pothos/plugin-with-input@3.10.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - '@prisma/client@5.22.0(prisma@5.22.0)': optionalDependencies: prisma: 5.22.0 - '@prisma/debug@5.15.0': {} - '@prisma/debug@5.22.0': {} '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} @@ -16805,10 +15147,6 @@ snapshots: '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 '@prisma/get-platform': 5.22.0 - '@prisma/generator-helper@5.15.0': - dependencies: - '@prisma/debug': 5.15.0 - '@prisma/get-platform@5.22.0': dependencies: '@prisma/debug': 5.22.0 @@ -16860,7 +15198,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.24.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.8.5 '@quri/squiggle-lang': 0.8.5 @@ -16877,7 +15215,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-error-boundary: 4.0.11(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 8.0.7(@types/react@18.3.3)(react@18.3.1) react-resizable: 3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vscode-uri: 3.0.8 @@ -16899,7 +15237,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.8.6 '@quri/squiggle-lang': 0.8.6 @@ -16917,7 +15255,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-error-boundary: 4.0.11(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) react-resizable: 3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vscode-uri: 3.0.8 @@ -16939,7 +15277,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.0 '@quri/squiggle-lang': 0.9.0 @@ -16957,7 +15295,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) zod: 3.23.8 transitivePeerDependencies: @@ -16977,7 +15315,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.2 '@quri/squiggle-lang': 0.9.2 @@ -16995,7 +15333,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) remark-gfm: 4.0.0 shikiji: 0.9.15 @@ -17019,7 +15357,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.3 '@quri/squiggle-lang': 0.9.3 @@ -17037,7 +15375,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) remark-gfm: 4.0.0 shikiji: 0.10.2 @@ -17061,7 +15399,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.4 '@quri/squiggle-lang': 0.9.4 @@ -17077,7 +15415,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) remark-gfm: 4.0.0 shikiji: 0.10.2 @@ -17101,7 +15439,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.5 '@quri/squiggle-lang': 0.9.5 @@ -17117,7 +15455,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) remark-gfm: 4.0.0 shikiji: 0.10.2 @@ -17211,7 +15549,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.2(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -17227,7 +15565,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.3(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -17243,7 +15581,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.3(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -17259,7 +15597,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.3(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -17275,7 +15613,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.3(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -17351,6 +15689,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -17364,6 +15709,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -17399,6 +15751,29 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.25.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + aria-hidden: 1.2.3 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -17441,6 +15816,20 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -17461,6 +15850,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -17479,6 +15875,18 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -17498,6 +15906,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-id@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -17578,6 +15994,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025) @@ -17599,6 +16025,17 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -17619,6 +16056,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -17670,6 +16117,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.25.6 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -17700,6 +16155,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -17714,6 +16176,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -17729,6 +16199,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -17743,6 +16221,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -17789,6 +16274,15 @@ snapshots: clsx: 2.1.1 react: 18.3.1 + '@react-aria/focus@3.17.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/interactions': 3.21.3(react@19.0.0-rc-66855b96-20241106) + '@react-aria/utils': 3.24.1(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.23.1(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 19.0.0-rc-66855b96-20241106 + '@react-aria/interactions@3.21.3(react@18.3.1)': dependencies: '@react-aria/ssr': 3.9.4(react@18.3.1) @@ -17797,11 +16291,24 @@ snapshots: '@swc/helpers': 0.5.15 react: 18.3.1 + '@react-aria/interactions@3.21.3(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/ssr': 3.9.4(react@19.0.0-rc-66855b96-20241106) + '@react-aria/utils': 3.24.1(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.23.1(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.15 + react: 19.0.0-rc-66855b96-20241106 + '@react-aria/ssr@3.9.4(react@18.3.1)': dependencies: '@swc/helpers': 0.5.15 react: 18.3.1 + '@react-aria/ssr@3.9.4(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@swc/helpers': 0.5.15 + react: 19.0.0-rc-66855b96-20241106 + '@react-aria/utils@3.24.1(react@18.3.1)': dependencies: '@react-aria/ssr': 3.9.4(react@18.3.1) @@ -17811,6 +16318,15 @@ snapshots: clsx: 2.1.1 react: 18.3.1 + '@react-aria/utils@3.24.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/ssr': 3.9.4(react@19.0.0-rc-66855b96-20241106) + '@react-stately/utils': 3.10.1(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.23.1(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 19.0.0-rc-66855b96-20241106 + '@react-hook/latest@1.0.3(react@18.3.1)': dependencies: react: 18.3.1 @@ -17837,33 +16353,42 @@ snapshots: '@swc/helpers': 0.5.15 react: 18.3.1 + '@react-stately/utils@3.10.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@swc/helpers': 0.5.15 + react: 19.0.0-rc-66855b96-20241106 + '@react-types/shared@3.23.1(react@18.3.1)': dependencies: react: 18.3.1 - '@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-types/shared@3.23.1(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + + '@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/controls@11.2.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/core@11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.2 @@ -17873,54 +16398,52 @@ snapshots: d3-drag: 3.0.0 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/minimap@11.7.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@types/d3-selection': 3.0.4 '@types/d3-zoom': 3.0.2 classcat: 5.0.5 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/node-resizer@2.2.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/node-toolbar@1.3.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@repeaterjs/repeater@3.0.4': {} - '@rollup/pluginutils@5.1.0(rollup@4.17.2)': dependencies: '@types/estree': 1.0.5 @@ -18048,9 +16571,9 @@ snapshots: memoizerific: 1.11.3 ts-dedent: 2.2.0 - '@storybook/addon-controls@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/addon-controls@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@storybook/blocks': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/blocks': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) dequal: 2.0.3 lodash: 4.17.21 ts-dedent: 2.2.0 @@ -18091,11 +16614,11 @@ snapshots: - prettier - supports-color - '@storybook/addon-essentials@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/addon-essentials@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/addon-actions': 8.1.6 '@storybook/addon-backgrounds': 8.1.6 - '@storybook/addon-controls': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/addon-controls': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/addon-docs': 8.1.6(@types/react-dom@18.3.0)(prettier@3.3.3) '@storybook/addon-highlight': 8.1.6 '@storybook/addon-measure': 8.1.6 @@ -18103,7 +16626,7 @@ snapshots: '@storybook/addon-toolbars': 8.1.6 '@storybook/addon-viewport': 8.1.6 '@storybook/core-common': 8.1.6(prettier@3.3.3) - '@storybook/manager-api': 8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/manager-api': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/node-logger': 8.1.6 '@storybook/preview-api': 8.1.6 ts-dedent: 2.2.0 @@ -18135,21 +16658,21 @@ snapshots: - jest - vitest - '@storybook/addon-links@8.0.9(react@18.3.1)': + '@storybook/addon-links@8.0.9(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/csf': 0.1.6 '@storybook/global': 5.0.0 ts-dedent: 2.2.0 optionalDependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 - '@storybook/addon-links@8.1.6(react@18.3.1)': + '@storybook/addon-links@8.1.6(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/csf': 0.1.8 '@storybook/global': 5.0.0 ts-dedent: 2.2.0 optionalDependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 '@storybook/addon-measure@8.1.6': dependencies: @@ -18167,35 +16690,35 @@ snapshots: dependencies: memoizerific: 1.11.3 - '@storybook/blocks@8.0.9(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/blocks@8.0.9(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/channels': 8.0.9 '@storybook/client-logger': 8.0.9 - '@storybook/components': 8.0.9(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/components': 8.0.9(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/core-events': 8.0.9 '@storybook/csf': 0.1.6 '@storybook/docs-tools': 8.0.9 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/manager-api': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/preview-api': 8.0.9 - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/theming': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.0.9 '@types/lodash': 4.17.13 color-convert: 2.0.1 dequal: 2.0.3 lodash: 4.17.21 - markdown-to-jsx: 7.3.2(react@18.3.1) + markdown-to-jsx: 7.3.2(react@19.0.0-rc-66855b96-20241106) memoizerific: 1.11.3 polished: 4.3.1 - react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-colorful: 5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) telejson: 7.2.0 tocbot: 4.27.18 ts-dedent: 2.2.0 util-deprecate: 1.0.2 optionalDependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - encoding @@ -18237,6 +16760,42 @@ snapshots: - prettier - supports-color + '@storybook/blocks@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@storybook/channels': 8.1.6 + '@storybook/client-logger': 8.1.6 + '@storybook/components': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/core-events': 8.1.6 + '@storybook/csf': 0.1.8 + '@storybook/docs-tools': 8.1.6(prettier@3.3.3) + '@storybook/global': 5.0.0 + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/manager-api': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/preview-api': 8.1.6 + '@storybook/theming': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/types': 8.1.6 + '@types/lodash': 4.17.13 + color-convert: 2.0.1 + dequal: 2.0.3 + lodash: 4.17.21 + markdown-to-jsx: 7.3.2(react@19.0.0-rc-66855b96-20241106) + memoizerific: 1.11.3 + polished: 4.3.1 + react-colorful: 5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + telejson: 7.2.0 + tocbot: 4.27.18 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + optionalDependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - encoding + - prettier + - supports-color + '@storybook/builder-manager@8.1.6(prettier@3.3.3)': dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 @@ -18327,7 +16886,7 @@ snapshots: telejson: 7.2.0 tiny-invariant: 1.3.3 - '@storybook/cli@8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/cli@8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@babel/core': 7.26.0 '@babel/types': 7.24.5 @@ -18335,7 +16894,7 @@ snapshots: '@storybook/codemod': 8.1.6 '@storybook/core-common': 8.1.6(prettier@3.3.3) '@storybook/core-events': 8.1.6 - '@storybook/core-server': 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/core-server': 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/csf-tools': 8.1.6 '@storybook/node-logger': 8.1.6 '@storybook/telemetry': 8.1.6(prettier@3.3.3) @@ -18402,18 +16961,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/components@8.0.9(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/components@8.0.9(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) '@storybook/client-logger': 8.0.9 '@storybook/csf': 0.1.6 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/theming': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.0.9 memoizerific: 1.11.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) util-deprecate: 1.0.2 transitivePeerDependencies: - '@types/react' @@ -18436,6 +16995,24 @@ snapshots: - '@types/react' - '@types/react-dom' + '@storybook/components@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@storybook/client-logger': 8.1.6 + '@storybook/csf': 0.1.8 + '@storybook/global': 5.0.0 + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/theming': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/types': 8.1.6 + memoizerific: 1.11.3 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + util-deprecate: 1.0.2 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + '@storybook/core-common@8.0.9': dependencies: '@storybook/core-events': 8.0.9 @@ -18516,7 +17093,7 @@ snapshots: '@storybook/csf': 0.1.8 ts-dedent: 2.2.0 - '@storybook/core-server@8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/core-server@8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@aw-web-design/x-default-browser': 1.4.126 '@babel/core': 7.26.0 @@ -18531,7 +17108,7 @@ snapshots: '@storybook/docs-mdx': 3.1.0-next.0 '@storybook/global': 5.0.0 '@storybook/manager': 8.1.6 - '@storybook/manager-api': 8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/manager-api': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/node-logger': 8.1.6 '@storybook/preview-api': 8.1.6 '@storybook/telemetry': 8.1.6(prettier@3.3.3) @@ -18660,6 +17237,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@storybook/icons@1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@storybook/instrumenter@8.1.6': dependencies: '@storybook/channels': 8.1.6 @@ -18670,16 +17252,16 @@ snapshots: '@vitest/utils': 1.5.3 util: 0.12.5 - '@storybook/manager-api@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/manager-api@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/channels': 8.0.9 '@storybook/client-logger': 8.0.9 '@storybook/core-events': 8.0.9 '@storybook/csf': 0.1.6 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/router': 8.0.9 - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/theming': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.0.9 dequal: 2.0.3 lodash: 4.17.21 @@ -18712,6 +17294,27 @@ snapshots: - react - react-dom + '@storybook/manager-api@8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@storybook/channels': 8.1.6 + '@storybook/client-logger': 8.1.6 + '@storybook/core-events': 8.1.6 + '@storybook/csf': 0.1.8 + '@storybook/global': 5.0.0 + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/router': 8.1.6 + '@storybook/theming': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/types': 8.1.6 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + store2: 2.14.3 + telejson: 7.2.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - react + - react-dom + '@storybook/manager@8.1.6': {} '@storybook/node-logger@8.0.9': {} @@ -18756,28 +17359,33 @@ snapshots: '@storybook/preview@8.1.6': {} - '@storybook/react-dom-shim@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/react-dom-shim@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) '@storybook/react-dom-shim@8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-vite@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0))': + '@storybook/react-dom-shim@8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + + '@storybook/react-vite@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0)) '@rollup/pluginutils': 5.1.0(rollup@4.17.2) '@storybook/builder-vite': 8.0.9(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0)) '@storybook/node-logger': 8.0.9 - '@storybook/react': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@storybook/react': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) find-up: 5.0.0 magic-string: 0.30.10 - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 react-docgen: 7.0.3 - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) resolve: 1.22.8 tsconfig-paths: 4.2.0 vite: 5.2.11(@types/node@22.9.0) @@ -18789,19 +17397,19 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react-vite@8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7))': + '@storybook/react-vite@8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7)) '@rollup/pluginutils': 5.1.0(rollup@4.17.2) '@storybook/builder-vite': 8.1.6(prettier@3.3.3)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7)) '@storybook/node-logger': 8.1.6 - '@storybook/react': 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@storybook/react': 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) '@storybook/types': 8.1.6 find-up: 5.0.0 magic-string: 0.30.10 - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 react-docgen: 7.0.3 - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) resolve: 1.22.8 tsconfig-paths: 4.2.0 vite: 5.2.10(@types/node@20.12.7) @@ -18814,13 +17422,13 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + '@storybook/react@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3)': dependencies: '@storybook/client-logger': 8.0.9 '@storybook/docs-tools': 8.0.9 '@storybook/global': 5.0.0 '@storybook/preview-api': 8.0.9 - '@storybook/react-dom-shim': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/react-dom-shim': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.0.9 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 @@ -18832,9 +17440,9 @@ snapshots: html-tags: 3.3.1 lodash: 4.17.21 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-element-to-jsx-string: 15.0.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) semver: 7.6.3 ts-dedent: 2.2.0 type-fest: 2.19.0 @@ -18845,13 +17453,13 @@ snapshots: - encoding - supports-color - '@storybook/react@8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + '@storybook/react@8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3)': dependencies: '@storybook/client-logger': 8.1.6 '@storybook/docs-tools': 8.1.6(prettier@3.3.3) '@storybook/global': 5.0.0 '@storybook/preview-api': 8.1.6 - '@storybook/react-dom-shim': 8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/react-dom-shim': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.1.6 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 @@ -18863,9 +17471,9 @@ snapshots: html-tags: 3.3.1 lodash: 4.17.21 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-element-to-jsx-string: 15.0.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) semver: 7.6.0 ts-dedent: 2.2.0 type-fest: 2.19.0 @@ -18929,15 +17537,15 @@ snapshots: '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) ts-dedent: 2.2.0 - '@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/theming@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.0.0-rc-66855b96-20241106) '@storybook/client-logger': 8.0.9 '@storybook/global': 5.0.0 memoizerific: 1.11.3 optionalDependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) '@storybook/theming@8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -18949,6 +17557,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@storybook/theming@8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.0.0-rc-66855b96-20241106) + '@storybook/client-logger': 8.1.6 + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + optionalDependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@storybook/types@8.0.9': dependencies: '@storybook/channels': 8.0.9 @@ -19006,6 +17624,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-virtual@3.10.8(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@tanstack/virtual-core': 3.10.8 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@tanstack/virtual-core@3.10.8': {} '@testing-library/dom@10.4.0': @@ -19060,12 +17684,12 @@ snapshots: '@types/jest': 29.5.14 jest: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) - '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@babel/runtime': 7.25.6 '@testing-library/dom': 10.4.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) optionalDependencies: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 @@ -19340,16 +17964,12 @@ snapshots: '@types/js-cookie@2.2.7': {} - '@types/js-yaml@4.0.5': {} - '@types/jsdom@20.0.1': dependencies: '@types/node': 22.9.0 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 - '@types/json-stable-stringify@1.0.34': {} - '@types/json5@0.0.29': {} '@types/katex@0.16.3': {} @@ -19419,11 +18039,6 @@ snapshots: dependencies: '@types/react': 18.3.3 - '@types/react-relay@16.0.6': - dependencies: - '@types/react': 18.3.3 - '@types/relay-runtime': 14.1.23 - '@types/react-transition-group@4.4.6': dependencies: '@types/react': 18.3.3 @@ -19433,8 +18048,6 @@ snapshots: '@types/prop-types': 15.7.11 csstype: 3.1.3 - '@types/relay-runtime@14.1.23': {} - '@types/resolve@1.20.6': {} '@types/semver@7.5.8': {} @@ -19466,10 +18079,6 @@ snapshots: '@types/vscode@1.86.0': {} - '@types/ws@8.5.5': - dependencies: - '@types/node': 22.9.0 - '@types/yargs-parser@21.0.0': {} '@types/yargs@17.0.24': @@ -19660,12 +18269,12 @@ snapshots: next: 15.0.2(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025) react: 19.0.0-rc-cae764ce-20241025 - '@vercel/analytics@1.3.1(next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/analytics@1.3.1(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 + next: 15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 '@vitest/expect@1.3.1': dependencies: @@ -19784,44 +18393,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@whatwg-node/events@0.0.3': {} - - '@whatwg-node/events@0.1.1': {} - - '@whatwg-node/fetch@0.8.8': - dependencies: - '@peculiar/webcrypto': 1.4.3 - '@whatwg-node/node-fetch': 0.3.6 - busboy: 1.6.0 - urlpattern-polyfill: 8.0.2 - web-streams-polyfill: 3.2.1 - - '@whatwg-node/fetch@0.9.7': - dependencies: - '@whatwg-node/node-fetch': 0.4.6 - urlpattern-polyfill: 9.0.0 - - '@whatwg-node/node-fetch@0.3.6': - dependencies: - '@whatwg-node/events': 0.0.3 - busboy: 1.6.0 - fast-querystring: 1.1.1 - fast-url-parser: 1.1.3 - tslib: 2.8.1 - - '@whatwg-node/node-fetch@0.4.6': - dependencies: - '@whatwg-node/events': 0.1.1 - busboy: 1.6.0 - fast-querystring: 1.1.1 - fast-url-parser: 1.1.3 - tslib: 2.8.1 - - '@whatwg-node/server@0.9.1': - dependencies: - '@whatwg-node/fetch': 0.9.7 - tslib: 2.6.2 - '@wogns3623/eslint-plugin-better-exhaustive-deps@1.1.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 @@ -20052,8 +18623,6 @@ snapshots: arrify@1.0.1: {} - asap@2.0.6: {} - asn1.js@5.4.1: dependencies: bn.js: 4.12.0 @@ -20061,12 +18630,6 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 - asn1js@3.0.5: - dependencies: - pvtsutils: 1.3.5 - pvutils: 1.1.3 - tslib: 2.8.1 - assert@2.1.0: dependencies: call-bind: 1.0.7 @@ -20083,8 +18646,6 @@ snapshots: dependencies: tslib: 2.8.1 - astral-regex@2.0.0: {} - astring@1.8.6: {} async@3.2.4: {} @@ -20095,8 +18656,6 @@ snapshots: asynckit@0.4.0: {} - auto-bind@4.0.0: {} - auto-bind@5.0.1: {} autoprefixer@10.4.19(postcss@8.4.38): @@ -20168,12 +18727,6 @@ snapshots: '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.5 - babel-plugin-macros@2.8.0: - dependencies: - '@babel/runtime': 7.23.9 - cosmiconfig: 6.0.0 - resolve: 1.22.8 - babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.26.0 @@ -20204,14 +18757,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-relay@16.2.0: - dependencies: - babel-plugin-macros: 2.8.0 - cosmiconfig: 5.2.1 - graphql: 15.3.0 - - babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: {} - babel-preset-current-node-syntax@1.0.1(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -20228,39 +18773,6 @@ snapshots: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) - babel-preset-fbjs@3.4.0(@babel/core@7.26.0): - dependencies: - '@babel/core': 7.26.0 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.26.0) - '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.26.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) - '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.26.0) - '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.26.0) - '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.26.0) - babel-plugin-syntax-trailing-function-commas: 7.0.0-beta.0 - transitivePeerDependencies: - - supports-color - babel-preset-jest@29.6.3(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -20401,13 +18913,6 @@ snapshots: node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) - browserslist@4.23.3: - dependencies: - caniuse-lite: 1.0.30001679 - electron-to-chromium: 1.5.13 - node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.23.3) - browserslist@4.24.2: dependencies: caniuse-lite: 1.0.30001679 @@ -20464,23 +18969,8 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 - caller-callsite@2.0.0: - dependencies: - callsites: 2.0.0 - - caller-path@2.0.0: - dependencies: - caller-callsite: 2.0.0 - - callsites@2.0.0: {} - callsites@3.1.0: {} - camel-case@4.1.2: - dependencies: - pascal-case: 3.1.2 - tslib: 2.8.1 - camelcase-css@2.0.1: {} camelcase-keys@6.2.2: @@ -20503,12 +18993,6 @@ snapshots: caniuse-lite@1.0.30001680: {} - capital-case@1.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - upper-case-first: 2.0.2 - ccount@2.0.1: {} chai@4.4.1: @@ -20539,34 +19023,6 @@ snapshots: chalk@5.3.0: {} - change-case-all@1.0.15: - dependencies: - change-case: 4.1.2 - is-lower-case: 2.0.2 - is-upper-case: 2.0.2 - lower-case: 2.0.2 - lower-case-first: 2.0.2 - sponge-case: 1.0.1 - swap-case: 2.0.2 - title-case: 3.0.3 - upper-case: 2.0.2 - upper-case-first: 2.0.2 - - change-case@4.1.2: - dependencies: - camel-case: 4.1.2 - capital-case: 1.0.4 - constant-case: 3.0.4 - dot-case: 3.0.4 - header-case: 2.0.4 - no-case: 3.0.4 - param-case: 3.0.4 - pascal-case: 3.1.2 - path-case: 3.0.4 - sentence-case: 3.0.4 - snake-case: 3.0.4 - tslib: 2.8.1 - char-regex@1.0.2: {} character-entities-html4@2.1.0: {} @@ -20669,18 +19125,11 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 - cli-truncate@2.1.0: - dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 - cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 string-width: 7.2.0 - cli-width@3.0.0: {} - client-only@0.0.1: {} cliui@6.0.0: @@ -20782,8 +19231,6 @@ snapshots: color-string: 1.9.1 optional: true - colorette@2.0.20: {} - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -20804,8 +19251,6 @@ snapshots: commander@9.5.0: {} - common-tags@1.8.2: {} - commondir@1.0.1: {} compressible@2.0.18: @@ -20837,12 +19282,6 @@ snapshots: consola@3.2.3: {} - constant-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - upper-case: 2.0.2 - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -20875,21 +19314,6 @@ snapshots: dependencies: layout-base: 1.0.2 - cosmiconfig@5.2.1: - dependencies: - import-fresh: 2.0.0 - is-directory: 0.3.1 - js-yaml: 3.14.1 - parse-json: 4.0.0 - - cosmiconfig@6.0.0: - dependencies: - '@types/parse-json': 4.0.1 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.1 @@ -20898,15 +19322,6 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@8.3.6(typescript@5.6.3): - dependencies: - import-fresh: 3.3.0 - js-yaml: 4.1.0 - parse-json: 5.2.0 - path-type: 4.0.0 - optionalDependencies: - typescript: 5.6.3 - create-ecdh@4.0.4: dependencies: bn.js: 4.12.0 @@ -20978,12 +19393,6 @@ snapshots: crelt@1.0.5: {} - cross-fetch@3.1.8: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -21265,16 +19674,12 @@ snapshots: dataloader@1.4.0: {} - dataloader@2.2.2: {} - date-fns@3.6.0: {} date-fns@4.1.0: {} dayjs@1.11.10: {} - debounce@1.2.1: {} - debug@2.6.9: dependencies: ms: 2.0.0 @@ -21518,11 +19923,6 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dot-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - dotenv-cli@7.4.2: dependencies: cross-spawn: 7.0.3 @@ -21534,8 +19934,6 @@ snapshots: dotenv@16.4.5: {} - dset@3.1.2: {} - duplexify@3.7.1: dependencies: end-of-stream: 1.4.4 @@ -21557,8 +19955,6 @@ snapshots: electron-to-chromium@1.4.752: {} - electron-to-chromium@1.5.13: {} - electron-to-chromium@1.5.51: {} elkjs@0.9.1: {} @@ -22249,14 +20645,10 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 - extract-files@11.0.0: {} - fast-check@3.19.0: dependencies: pure-rand: 6.1.0 - fast-decode-uri-component@1.0.1: {} - fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -22273,10 +20665,6 @@ snapshots: fast-loops@1.1.3: {} - fast-querystring@1.1.1: - dependencies: - fast-decode-uri-component: 1.0.1 - fast-shallow-equal@1.0.0: {} fast-url-parser@1.1.3: @@ -22299,20 +20687,6 @@ snapshots: dependencies: bser: 2.1.1 - fbjs-css-vars@1.0.2: {} - - fbjs@3.0.5: - dependencies: - cross-fetch: 3.1.8 - fbjs-css-vars: 1.0.2 - loose-envify: 1.4.0 - object-assign: 4.1.1 - promise: 7.3.1 - setimmediate: 1.0.5 - ua-parser-js: 1.0.37 - transitivePeerDependencies: - - encoding - fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -22323,10 +20697,6 @@ snapshots: fetch-retry@5.0.6: {} - figures@3.2.0: - dependencies: - escape-string-regexp: 1.0.5 - figures@5.0.0: dependencies: escape-string-regexp: 5.0.0 @@ -22452,6 +20822,14 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + framer-motion@11.0.3(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + tslib: 2.6.2 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + fresh@0.5.2: {} fs-constants@1.0.0: {} @@ -22742,63 +21120,6 @@ snapshots: graphemer@1.4.0: {} - graphql-config@5.0.2(@types/node@20.12.7)(graphql@16.8.1)(typescript@5.6.3): - dependencies: - '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/json-file-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/load': 8.0.0(graphql@16.8.1) - '@graphql-tools/merge': 9.0.0(graphql@16.8.1) - '@graphql-tools/url-loader': 8.0.0(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - cosmiconfig: 8.3.6(typescript@5.6.3) - graphql: 16.8.1 - jiti: 1.21.0 - minimatch: 4.2.3 - string-env-interpolation: 1.0.1 - tslib: 2.6.2 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - typescript - - utf-8-validate - - graphql-request@6.1.0(graphql@16.8.1): - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - cross-fetch: 3.1.8 - graphql: 16.8.1 - transitivePeerDependencies: - - encoding - - graphql-tag@2.12.6(graphql@16.8.1): - dependencies: - graphql: 16.8.1 - tslib: 2.8.1 - - graphql-ws@5.14.0(graphql@16.8.1): - dependencies: - graphql: 16.8.1 - - graphql-yoga@5.1.1(graphql@16.8.1): - dependencies: - '@envelop/core': 5.0.0 - '@graphql-tools/executor': 1.1.0(graphql@16.8.1) - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@graphql-yoga/logger': 2.0.0 - '@graphql-yoga/subscription': 5.0.0 - '@whatwg-node/fetch': 0.9.7 - '@whatwg-node/server': 0.9.1 - dset: 3.1.2 - graphql: 16.8.1 - lru-cache: 10.0.0 - tslib: 2.6.2 - - graphql@15.3.0: {} - - graphql@16.8.1: {} - gray-matter@4.0.3: dependencies: js-yaml: 3.14.1 @@ -22998,11 +21319,6 @@ snapshots: property-information: 6.4.0 space-separated-tokens: 2.0.2 - header-case@2.0.4: - dependencies: - capital-case: 1.0.4 - tslib: 2.8.1 - heap@0.2.7: {} hmac-drbg@1.0.1: @@ -23072,13 +21388,6 @@ snapshots: transitivePeerDependencies: - supports-color - http-proxy-agent@7.0.0: - dependencies: - agent-base: 7.1.0 - debug: 4.3.6 - transitivePeerDependencies: - - supports-color - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 @@ -23093,13 +21402,6 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.0 - debug: 4.3.6 - transitivePeerDependencies: - - supports-color - https-proxy-agent@7.0.4: dependencies: agent-base: 7.1.0 @@ -23152,22 +21454,13 @@ snapshots: dependencies: queue: 6.0.2 - immutable@3.7.6: {} - immutable@4.3.6: {} - import-fresh@2.0.0: - dependencies: - caller-path: 2.0.0 - resolve-from: 3.0.0 - import-fresh@3.3.0: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - import-from@4.0.0: {} - import-local@3.1.0: dependencies: pkg-dir: 4.2.0 @@ -23235,24 +21528,6 @@ snapshots: css-in-js-utils: 3.1.0 fast-loops: 1.1.3 - inquirer@8.2.6: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 - internal-slot@1.0.5: dependencies: get-intrinsic: 1.2.2 @@ -23271,11 +21546,6 @@ snapshots: is-absolute-url@4.0.1: {} - is-absolute@1.0.0: - dependencies: - is-relative: 1.0.0 - is-windows: 1.0.2 - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -23332,8 +21602,6 @@ snapshots: is-deflate@1.0.0: {} - is-directory@0.3.1: {} - is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -23376,10 +21644,6 @@ snapshots: is-interactive@1.0.0: {} - is-lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - is-map@2.0.2: {} is-nan@1.3.2: @@ -23424,10 +21688,6 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.0 - is-relative@1.0.0: - dependencies: - is-unc-path: 1.0.0 - is-set@2.0.2: {} is-shared-array-buffer@1.0.2: @@ -23458,18 +21718,10 @@ snapshots: dependencies: which-typed-array: 1.1.15 - is-unc-path@1.0.0: - dependencies: - unc-path-regex: 0.1.2 - is-unicode-supported@0.1.0: {} is-unicode-supported@1.3.0: {} - is-upper-case@2.0.2: - dependencies: - tslib: 2.8.1 - is-weakmap@2.0.1: {} is-weakref@1.0.2: @@ -23507,14 +21759,6 @@ snapshots: isobject@3.0.1: {} - isomorphic-ws@5.0.0(ws@8.14.1): - dependencies: - ws: 8.14.1 - - isomorphic-ws@5.0.0(ws@8.16.0): - dependencies: - ws: 8.16.0 - istanbul-lib-coverage@3.2.0: {} istanbul-lib-instrument@5.2.1: @@ -24030,12 +22274,8 @@ snapshots: - supports-color - ts-node - jiti@1.20.0: {} - jiti@1.21.0: {} - jose@4.15.5: {} - jose@5.9.6: {} js-cookie@2.2.1: {} @@ -24147,8 +22387,6 @@ snapshots: json-buffer@3.0.1: {} - json-parse-better-errors@1.0.2: {} - json-parse-even-better-errors@2.3.1: {} json-parse-even-better-errors@3.0.2: {} @@ -24157,15 +22395,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-stable-stringify@1.0.2: - dependencies: - jsonify: 0.0.1 - - json-to-pretty-yaml@1.2.2: - dependencies: - remedial: 1.0.8 - remove-trailing-spaces: 1.0.8 - json5@1.0.2: dependencies: minimist: 1.2.8 @@ -24184,8 +22413,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonify@0.0.1: {} - jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 @@ -24346,19 +22573,6 @@ snapshots: dependencies: uc.micro: 1.0.6 - listr2@4.0.5(enquirer@2.3.6): - dependencies: - cli-truncate: 2.1.0 - colorette: 2.0.20 - log-update: 4.0.0 - p-map: 4.0.0 - rfdc: 1.3.0 - rxjs: 7.8.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - optionalDependencies: - enquirer: 2.3.6 - lite-emit@2.3.0: {} load-plugin@6.0.3: @@ -24412,8 +22626,6 @@ snapshots: lodash.once@4.1.1: {} - lodash.sortby@4.7.0: {} - lodash.startcase@4.4.0: {} lodash@4.17.21: {} @@ -24423,13 +22635,6 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-update@4.0.0: - dependencies: - ansi-escapes: 4.3.2 - cli-cursor: 3.1.0 - slice-ansi: 4.0.0 - wrap-ansi: 6.2.0 - longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -24437,18 +22642,8 @@ snapshots: js-tokens: 4.0.0 loupe@2.3.7: - dependencies: - get-func-name: 2.0.2 - - lower-case-first@2.0.2: - dependencies: - tslib: 2.8.1 - - lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - - lru-cache@10.0.0: {} + dependencies: + get-func-name: 2.0.2 lru-cache@10.2.2: {} @@ -24496,8 +22691,6 @@ snapshots: dependencies: tmpl: 1.0.5 - map-cache@0.2.2: {} - map-obj@1.0.1: {} map-obj@4.3.0: {} @@ -24520,6 +22713,10 @@ snapshots: dependencies: react: 18.3.1 + markdown-to-jsx@7.3.2(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + md5.js@1.3.5: dependencies: hash-base: 3.1.0 @@ -24826,10 +23023,6 @@ snapshots: transitivePeerDependencies: - supports-color - meros@1.3.0(@types/node@20.12.7): - optionalDependencies: - '@types/node': 20.12.7 - methods@1.1.2: {} micromark-core-commonmark@1.1.0: @@ -25285,10 +23478,6 @@ snapshots: dependencies: brace-expansion: 1.1.11 - minimatch@4.2.3: - dependencies: - brace-expansion: 1.1.11 - minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 @@ -25361,6 +23550,19 @@ snapshots: stacktrace-js: 2.0.2 stylis: 4.3.0 + nano-css@5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + css-tree: 1.1.3 + csstype: 3.1.3 + fastest-stable-stringify: 2.0.2 + inline-style-prefixer: 7.0.0 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + rtl-css-js: 1.16.1 + stacktrace-js: 2.0.2 + stylis: 4.3.0 + nanoid@3.3.7: {} napi-build-utils@1.0.2: @@ -25374,14 +23576,22 @@ snapshots: neo-async@2.6.2: {} - next-auth@5.0.0-beta.25(next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.13)(react@18.3.1): + next-auth@5.0.0-beta.25(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(nodemailer@6.9.13)(react@19.0.0-rc-66855b96-20241106): dependencies: '@auth/core': 0.37.2(nodemailer@6.9.13) - next: 15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 + next: 15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 optionalDependencies: nodemailer: 6.9.13 + next-safe-action@7.9.9(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(zod@3.23.8): + dependencies: + next: 15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + zod: 3.23.8 + next-themes@0.3.0(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -25397,7 +23607,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react@18.3.1) + styled-jsx: 5.1.6(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 15.0.0 '@next/swc-darwin-x64': 15.0.0 @@ -25438,7 +23648,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: '@next/env': 15.0.3 '@swc/counter': 0.1.3 @@ -25446,9 +23656,9 @@ snapshots: busboy: 1.6.0 caniuse-lite: 1.0.30001680 postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + styled-jsx: 5.1.6(babel-plugin-macros@3.1.0)(react@19.0.0-rc-66855b96-20241106) optionalDependencies: '@next/swc-darwin-arm64': 15.0.3 '@next/swc-darwin-x64': 15.0.3 @@ -25463,11 +23673,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.8.1 - node-abi@3.47.0: dependencies: semver: 7.6.3 @@ -25534,10 +23739,6 @@ snapshots: semver: 7.6.3 validate-npm-package-license: 3.0.4 - normalize-path@2.1.1: - dependencies: - remove-trailing-separator: 1.1.0 - normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -25574,8 +23775,6 @@ snapshots: dependencies: boolbase: 1.0.0 - nullthrows@1.1.1: {} - nwsapi@2.2.10: {} nwsapi@2.2.13: {} @@ -25779,11 +23978,6 @@ snapshots: pako@2.1.0: {} - param-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.8.1 - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -25807,17 +24001,6 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - parse-filepath@1.0.2: - dependencies: - is-absolute: 1.0.0 - map-cache: 0.2.2 - path-root: 0.1.1 - - parse-json@4.0.0: - dependencies: - error-ex: 1.3.2 - json-parse-better-errors: 1.0.2 - parse-json@5.2.0: dependencies: '@babel/code-frame': 7.24.7 @@ -25842,20 +24025,10 @@ snapshots: parseurl@1.3.3: {} - pascal-case@3.1.2: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - patch-console@2.0.0: {} path-browserify@1.0.1: {} - path-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.8.1 - path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -25868,12 +24041,6 @@ snapshots: path-parse@1.0.7: {} - path-root-regex@0.1.2: {} - - path-root@0.1.1: - dependencies: - path-root-regex: 0.1.2 - path-scurry@1.10.2: dependencies: lru-cache: 10.2.2 @@ -26125,10 +24292,6 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 - promise@7.3.1: - dependencies: - asap: 2.0.6 - prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -26190,12 +24353,6 @@ snapshots: pure-rand@6.1.0: {} - pvtsutils@1.3.5: - dependencies: - tslib: 2.8.1 - - pvutils@1.1.3: {} - qs@6.11.0: dependencies: side-channel: 1.0.6 @@ -26247,6 +24404,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-colorful@5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-docgen-typescript@2.2.2(typescript@5.6.3): dependencies: typescript: 5.6.3 @@ -26272,6 +24434,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + scheduler: 0.25.0-rc-66855b96-20241106 + react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -26284,12 +24451,19 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-element-to-jsx-string@15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-draggable@4.4.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + + react-element-to-jsx-string@15.0.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: '@base2/pretty-print-object': 1.0.1 is-plain-object: 5.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) react-is: 18.1.0 react-error-boundary@4.0.11(react@18.3.1): @@ -26297,13 +24471,17 @@ snapshots: '@babel/runtime': 7.25.6 react: 18.3.1 - react-hook-form@7.50.0(react@18.3.1): + react-hook-form@7.53.2(react@18.3.1): dependencies: react: 18.3.1 - react-icons@5.2.1(react@18.3.1): + react-hook-form@7.53.2(react@19.0.0-rc-66855b96-20241106): dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 + + react-icons@5.2.1(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 react-icons@5.2.1(react@19.0.0-rc-cae764ce-20241025): dependencies: @@ -26317,9 +24495,9 @@ snapshots: react-is@18.2.0: {} - react-loading-skeleton@3.4.0(react@18.3.1): + react-loading-skeleton@3.4.0(react@19.0.0-rc-66855b96-20241106): dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 react-markdown@8.0.7(@types/react@18.3.3)(react@18.3.1): dependencies: @@ -26360,6 +24538,23 @@ snapshots: transitivePeerDependencies: - supports-color + react-markdown@9.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@types/hast': 3.0.4 + '@types/react': 18.3.3 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.2.0 + html-url-attributes: 3.0.0 + mdast-util-to-hast: 13.0.2 + react: 19.0.0-rc-66855b96-20241106 + remark-parse: 11.0.0 + remark-rehype: 11.0.0 + unified: 11.0.4 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + transitivePeerDependencies: + - supports-color + react-medium-image-zoom@5.2.10(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -26371,17 +24566,6 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-relay@16.2.0(react@18.3.1): - dependencies: - '@babel/runtime': 7.23.9 - fbjs: 3.0.5 - invariant: 2.2.4 - nullthrows: 1.1.1 - react: 18.3.1 - relay-runtime: 16.2.0 - transitivePeerDependencies: - - encoding - react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -26390,6 +24574,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.3 + react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -26409,6 +24601,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-remove-scroll@2.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + tslib: 2.8.1 + use-callback-ref: 1.3.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + use-sidecar: 1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + react-remove-scroll@2.6.0(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -26455,6 +24658,22 @@ snapshots: transitivePeerDependencies: - '@types/react' + react-select@5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@babel/runtime': 7.23.9 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.0(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/dom': 1.6.1 + '@types/react-transition-group': 4.4.6 + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-transition-group: 4.4.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + transitivePeerDependencies: + - '@types/react' + react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -26464,6 +24683,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-style-singleton@2.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.3 + react-style-singleton@2.2.1(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025): dependencies: get-nonce: 1.0.1 @@ -26491,12 +24719,12 @@ snapshots: transitivePeerDependencies: - '@types/react' - react-textarea-autosize@8.5.4(@types/react@18.3.3)(react@18.3.1): + react-textarea-autosize@8.5.4(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): dependencies: '@babel/runtime': 7.25.6 - react: 18.3.1 - use-composed-ref: 1.3.0(react@18.3.1) - use-latest: 1.2.1(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + use-composed-ref: 1.3.0(react@19.0.0-rc-66855b96-20241106) + use-latest: 1.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' @@ -26509,11 +24737,25 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-transition-group@4.4.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@babel/runtime': 7.23.9 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-universal-interface@0.6.2(react@18.3.1)(tslib@2.6.2): dependencies: react: 18.3.1 tslib: 2.6.2 + react-universal-interface@0.6.2(react@19.0.0-rc-66855b96-20241106)(tslib@2.6.2): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.6.2 + react-use@17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@types/js-cookie': 2.2.7 @@ -26533,22 +24775,43 @@ snapshots: ts-easing: 0.2.0 tslib: 2.6.2 + react-use@17.5.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@types/js-cookie': 2.2.7 + '@xobotyi/scrollbar-width': 1.9.5 + copy-to-clipboard: 3.3.3 + fast-deep-equal: 3.1.3 + fast-shallow-equal: 1.0.0 + js-cookie: 2.2.1 + nano-css: 5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-universal-interface: 0.6.2(react@19.0.0-rc-66855b96-20241106)(tslib@2.6.2) + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + set-harmonic-interval: 1.0.1 + throttle-debounce: 3.0.1 + ts-easing: 0.2.0 + tslib: 2.6.2 + react@18.3.1: dependencies: loose-envify: 1.4.0 + react@19.0.0-rc-66855b96-20241106: {} + react@19.0.0-rc-cae764ce-20241025: {} - reactflow@11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + reactflow@11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: - '@reactflow/background': 11.3.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/controls': 11.2.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/minimap': 11.7.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-resizer': 2.2.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@reactflow/background': 11.3.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/controls': 11.2.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/minimap': 11.7.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/node-resizer': 2.2.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer @@ -26775,24 +25038,6 @@ snapshots: hast-util-to-string: 3.0.0 unist-util-visit: 5.0.0 - relay-compiler@16.2.0: {} - - relay-runtime@12.0.0: - dependencies: - '@babel/runtime': 7.26.0 - fbjs: 3.0.5 - invariant: 2.2.4 - transitivePeerDependencies: - - encoding - - relay-runtime@16.2.0: - dependencies: - '@babel/runtime': 7.23.9 - fbjs: 3.0.5 - invariant: 2.2.4 - transitivePeerDependencies: - - encoding - remark-breaks@4.0.0: dependencies: '@types/mdast': 4.0.2 @@ -26891,12 +25136,6 @@ snapshots: transitivePeerDependencies: - supports-color - remedial@1.0.8: {} - - remove-trailing-separator@1.1.0: {} - - remove-trailing-spaces@1.0.8: {} - require-directory@2.1.1: {} require-main-filename@2.0.0: {} @@ -26909,8 +25148,6 @@ snapshots: dependencies: resolve-from: 5.0.0 - resolve-from@3.0.0: {} - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -26945,8 +25182,6 @@ snapshots: reusify@1.0.4: {} - rfdc@1.3.0: {} - rimraf@2.6.3: dependencies: glob: 7.2.3 @@ -27003,18 +25238,12 @@ snapshots: run-applescript@7.0.0: {} - run-async@2.4.1: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 rw@1.3.3: {} - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - sade@1.8.1: dependencies: mri: 1.2.0 @@ -27048,6 +25277,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + scheduler@0.25.0-rc-66855b96-20241106: {} + scheduler@0.25.0-rc-cae764ce-20241025: {} screenfull@5.2.0: {} @@ -27056,8 +25287,6 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.0 - scuid@1.1.0: {} - section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -27097,12 +25326,6 @@ snapshots: transitivePeerDependencies: - supports-color - sentence-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - upper-case-first: 2.0.2 - serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -27145,8 +25368,6 @@ snapshots: set-harmonic-interval@1.0.1: {} - setimmediate@1.0.5: {} - setprototypeof@1.2.0: {} sha.js@2.4.11: @@ -27197,8 +25418,6 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.1: {} - shiki@0.14.7: dependencies: ansi-sequence-parser: 1.1.0 @@ -27244,8 +25463,6 @@ snapshots: signal-exit@4.1.0: {} - signedsource@1.0.0: {} - simple-concat@1.0.1: optional: true @@ -27273,18 +25490,6 @@ snapshots: slash@5.1.0: {} - slice-ansi@3.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - - slice-ansi@4.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.1 @@ -27304,11 +25509,6 @@ snapshots: wcwidth: 1.0.1 yargs: 15.4.1 - snake-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.8.1 - source-map-generator@0.8.0: {} source-map-js@1.2.0: {} @@ -27354,10 +25554,6 @@ snapshots: spdx-license-ids@3.0.12: {} - sponge-case@1.0.1: - dependencies: - tslib: 2.8.1 - sprintf-js@1.0.3: {} stack-generator@2.0.10: @@ -27387,9 +25583,9 @@ snapshots: store2@2.14.3: {} - storybook@8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + storybook@8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: - '@storybook/cli': 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/cli': 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@babel/preset-env' - bufferutil @@ -27411,8 +25607,6 @@ snapshots: streamsearch@1.1.0: {} - string-env-interpolation@1.0.1: {} - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -27527,14 +25721,19 @@ snapshots: dependencies: inline-style-parser: 0.1.1 - styled-jsx@5.1.6(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react@18.3.1): + styled-jsx@5.1.6(babel-plugin-macros@3.1.0)(react@19.0.0-rc-66855b96-20241106): dependencies: client-only: 0.0.1 - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 optionalDependencies: - '@babel/core': 7.26.0 babel-plugin-macros: 3.1.0 + styled-jsx@5.1.6(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optional: true + styled-jsx@5.1.6(react@19.0.0-rc-cae764ce-20241025): dependencies: client-only: 0.0.1 @@ -27568,10 +25767,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swap-case@2.0.2: - dependencies: - tslib: 2.8.1 - symbol-tree@3.2.4: {} synckit@0.8.5: @@ -27755,8 +25950,6 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 - through@2.3.8: {} - tiny-glob@0.2.9: dependencies: globalyzer: 0.1.0 @@ -27771,10 +25964,6 @@ snapshots: tinyspy@2.2.1: {} - title-case@3.0.3: - dependencies: - tslib: 2.8.1 - titleize@3.0.0: {} tldts-core@6.1.58: {} @@ -27848,8 +26037,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-log@2.2.5: {} - ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -28037,8 +26224,6 @@ snapshots: typescript@5.6.3: {} - ua-parser-js@1.0.37: {} - uc.micro@1.0.6: {} ufo@1.3.2: {} @@ -28053,8 +26238,6 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 - unc-path-regex@0.1.2: {} - undefsafe@2.0.5: {} underscore@1.13.6: {} @@ -28187,10 +26370,6 @@ snapshots: universalify@2.0.1: {} - unixify@1.0.0: - dependencies: - normalize-path: 2.1.1 - unpipe@1.0.0: {} unplugin@1.10.1: @@ -28208,26 +26387,12 @@ snapshots: escalade: 3.1.1 picocolors: 1.1.1 - update-browserslist-db@1.1.0(browserslist@4.23.3): - dependencies: - browserslist: 4.23.3 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 escalade: 3.2.0 picocolors: 1.1.1 - upper-case-first@2.0.2: - dependencies: - tslib: 2.8.1 - - upper-case@2.0.2: - dependencies: - tslib: 2.8.1 - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -28243,10 +26408,6 @@ snapshots: dependencies: fast-url-parser: 1.1.3 - urlpattern-polyfill@8.0.2: {} - - urlpattern-polyfill@9.0.0: {} - use-callback-ref@1.3.2(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -28254,6 +26415,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + use-callback-ref@1.3.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.3 + use-callback-ref@1.3.2(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -28265,12 +26433,22 @@ snapshots: dependencies: react: 18.3.1 + use-composed-ref@1.3.0(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 + use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + use-latest@1.2.1(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -28278,6 +26456,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + use-latest@1.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + use-sidecar@1.1.2(@types/react@18.3.3)(react@18.3.1): dependencies: detect-node-es: 1.1.0 @@ -28286,6 +26471,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + use-sidecar@1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + detect-node-es: 1.1.0 + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.3 + use-sidecar@1.1.2(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025): dependencies: detect-node-es: 1.1.0 @@ -28294,9 +26487,9 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - use-sync-external-store@1.2.2(react@18.3.1): + use-sync-external-store@1.2.2(react@19.0.0-rc-66855b96-20241106): dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 use-sync-external-store@1.2.2(react@19.0.0-rc-cae764ce-20241025): dependencies: @@ -28340,8 +26533,6 @@ snapshots: validate-npm-package-name@5.0.1: {} - value-or-promise@1.0.12: {} - vary@1.1.2: {} vfile-location@5.0.2: @@ -28456,20 +26647,10 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.2.1: {} - web-streams-polyfill@4.0.0-beta.3: {} web-worker@1.2.0: {} - webcrypto-core@1.7.7: - dependencies: - '@peculiar/asn1-schema': 2.3.6 - '@peculiar/json-schema': 1.1.12 - asn1js: 3.0.5 - pvtsutils: 1.3.5 - tslib: 2.8.1 - webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -28613,10 +26794,6 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@8.14.1: {} - - ws@8.16.0: {} - ws@8.17.0: {} ws@8.18.0: {} @@ -28659,8 +26836,6 @@ snapshots: yallist@4.0.0: {} - yaml-ast-parser@0.0.43: {} - yaml@1.10.2: {} yaml@2.3.4: {} @@ -28719,11 +26894,11 @@ snapshots: zod@3.23.8: {} - zustand@4.5.5(@types/react@18.3.3)(react@18.3.1): + zustand@4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): dependencies: - use-sync-external-store: 1.2.2(react@18.3.1) + use-sync-external-store: 1.2.2(react@19.0.0-rc-66855b96-20241106) optionalDependencies: '@types/react': 18.3.3 - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 zwitch@2.0.4: {}