diff --git a/packages/hub/schema.graphql b/packages/hub/schema.graphql index e684767a0f..a6850dd998 100644 --- a/packages/hub/schema.graphql +++ b/packages/hub/schema.graphql @@ -152,6 +152,7 @@ enum MembershipRole { type Model implements Node { createdAtTimestamp: Float! currentRevision: ModelRevision! + exportRevisions(after: String, before: String, first: Int, last: Int, variableId: String!): ModelExportRevisionsConnection! id: ID! isEditable: Boolean! isPrivate: Boolean! @@ -183,6 +184,31 @@ type ModelExport implements Node { variableType: String! } +type ModelExportConnection { + edges: [ModelExportEdge!]! + pageInfo: PageInfo! +} + +type ModelExportEdge { + cursor: String! + node: ModelExport! +} + +input ModelExportQueryInput { + modelId: String + variableName: String +} + +type ModelExportRevisionsConnection { + edges: [ModelExportRevisionsConnectionEdge!]! + pageInfo: PageInfo! +} + +type ModelExportRevisionsConnectionEdge { + cursor: String! + node: ModelExport! +} + type ModelRevision implements Node { author: User comment: String! @@ -494,6 +520,7 @@ type Query { groups(after: String, before: String, first: Int, input: GroupsQueryInput, last: Int): GroupConnection! me: Me! model(input: QueryModelInput!): QueryModelResult! + modelExports(after: String, before: String, first: Int, input: ModelExportQueryInput, last: Int): ModelExportConnection! models(after: String, before: String, first: Int, last: Int): ModelConnection! """Admin-only query for listing models in /admin UI""" diff --git a/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/ModelExportPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/ModelExportPage.tsx index 6876838233..946ff4a82e 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/ModelExportPage.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/ModelExportPage.tsx @@ -1,15 +1,57 @@ "use client"; -import { FC } from "react"; -import { graphql } from "react-relay"; +import clsx from "clsx"; +import { format } from "date-fns"; +import { FC, useState } from "react"; +import { graphql, usePaginationFragment } from "react-relay"; +import { LoadMore } from "@/components/LoadMore"; import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; import { usePageQuery } from "@/relay/usePageQuery"; import { SquiggleModelExportPage } from "./SquiggleModelExportPage"; +import { + ModelExportPage$data, + ModelExportPage$key, +} from "@/__generated__/ModelExportPage.graphql"; import { ModelExportPageQuery } from "@/__generated__/ModelExportPageQuery.graphql"; +type ExportRevisions = ModelExportPage$data["exportRevisions"]; + +const RevisionsPanel: FC<{ + exportRevisions: ExportRevisions; + selected: string; + changeId: (id: string) => void; + loadNext?: (count: number) => void; +}> = ({ exportRevisions, selected, changeId, loadNext }) => { + return ( +
+

+ Revisions +

+ + {loadNext && } +
+ ); +}; + export const ModelExportPage: FC<{ params: { owner: string; @@ -20,39 +62,98 @@ export const ModelExportPage: FC<{ }> = ({ query, params }) => { const [{ model: result }] = usePageQuery( graphql` - query ModelExportPageQuery($input: QueryModelInput!) { + query ModelExportPageQuery( + $input: QueryModelInput! + $variableName: String! + ) { model(input: $input) { __typename ... on Model { id slug - currentRevision { + ...ModelExportPage @arguments(variableName: $variableName) + } + } + } + `, + query + ); + + const model = extractFromGraphqlErrorUnion(result, "Model"); + + const { + data: { exportRevisions }, + loadNext, + } = usePaginationFragment( + graphql` + fragment ModelExportPage on Model + @argumentDefinitions( + cursor: { type: "String" } + count: { type: "Int", defaultValue: 20 } + variableName: { type: "String!" } + ) + @refetchable(queryName: "ModelExportPagePaginationQuery") { + exportRevisions( + first: $count + after: $cursor + variableId: $variableName + ) @connection(key: "ModelExportPage_exportRevisions") { + edges { + node { id - content { - __typename - ...SquiggleModelExportPage + variableName + modelRevision { + id + createdAtTimestamp + content { + __typename + ...SquiggleModelExportPage + } } } } + pageInfo { + hasNextPage + } } } `, - query + model ); - const model = extractFromGraphqlErrorUnion(result, "Model"); - const content = model.currentRevision.content; - - switch (content.__typename) { - case "SquiggleSnippet": { - return ( - - ); + const [selected, changeId] = useState( + exportRevisions.edges.at(0)?.node.id || "" + ); + + const content = exportRevisions.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}
; } - default: - return
Unknown model type {content.__typename}
; } }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/page.tsx index 69cabca6f0..d43b0b24d5 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/exports/[variableName]/page.tsx @@ -13,6 +13,7 @@ type Props = { export default async function OuterModelExportPage({ params }: Props) { const query = await loadPageQuery(QueryNode, { input: { owner: params.owner, slug: params.slug }, + variableName: params.variableName, }); return ( diff --git a/packages/hub/src/components/LoadMore.tsx b/packages/hub/src/components/LoadMore.tsx index cae22885cc..f5bb8beed4 100644 --- a/packages/hub/src/components/LoadMore.tsx +++ b/packages/hub/src/components/LoadMore.tsx @@ -2,14 +2,19 @@ import { FC } from "react"; import { Button } from "@quri/ui"; +import { ButtonProps } from "../../../ui/dist/components/Button"; + type Props = { loadNext: (count: number) => void; + size?: ButtonProps["size"]; }; -export const LoadMore: FC = ({ loadNext }) => { +export const LoadMore: FC = ({ loadNext, size }) => { return (
- +
); }; diff --git a/packages/hub/src/graphql/queries/exports.ts b/packages/hub/src/graphql/queries/exports.ts new file mode 100644 index 0000000000..0c08512c16 --- /dev/null +++ b/packages/hub/src/graphql/queries/exports.ts @@ -0,0 +1,51 @@ +import { builder } from "@/graphql/builder"; +import { prisma } from "@/prisma"; + +import { ModelExport, ModelExportConnection } from "../types/ModelExport"; + +const ModelExportQueryInput = builder.inputType("ModelExportQueryInput", { + fields: (t) => ({ + modelId: t.string(), // Add this field to filter by model ID + variableName: t.string(), + }), +}); + +builder.queryField("modelExports", (t) => + t.prismaConnection( + { + type: ModelExport, + cursor: "id", + args: { + input: t.arg({ type: ModelExportQueryInput }), + }, + resolve: (query, _, { input }, { session }) => { + const modelId = input?.modelId; + if (!modelId) { + return []; + } + return prisma.modelExport.findMany({ + ...query, + where: { + ...(input.modelId && { + modelRevision: { + modelId: modelId, + }, + }), + ...(input.variableName && { + variableName: input.variableName, + }), + }, + orderBy: { + modelRevision: { + createdAt: "desc", + }, + }, + include: { + modelRevision: true, + }, + }); + }, + }, + ModelExportConnection + ) +); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 29395fed15..2ddfe7f5cb 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -6,6 +6,7 @@ import "./queries/group"; import "./queries/groups"; import "./queries/me"; import "./queries/model"; +import "./queries/exports"; import "./queries/models"; import "./queries/modelsByVersion"; import "./queries/relativeValuesDefinition"; diff --git a/packages/hub/src/graphql/types/Model.ts b/packages/hub/src/graphql/types/Model.ts index 1665c5ef4c..7da25cb967 100644 --- a/packages/hub/src/graphql/types/Model.ts +++ b/packages/hub/src/graphql/types/Model.ts @@ -3,6 +3,7 @@ import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; import { builder } from "@/graphql/builder"; import { decodeGlobalIdWithTypename } from "../utils"; +import { ModelExport } from "./ModelExport"; import { ModelRevision, ModelRevisionConnection } from "./ModelRevision"; import { Owner } from "./Owner"; @@ -62,6 +63,7 @@ export const Model = builder.prismaNode("Model", { currentRevision: t.relation("currentRevision", { nullable: false, }), + revision: t.field({ type: ModelRevision, args: { @@ -97,6 +99,19 @@ export const Model = builder.prismaNode("Model", { }, ModelRevisionConnection ), + exportRevisions: t.connection({ + type: ModelExport, + args: exportRevisionConnectionHelpers.getArgs(), + select: (args, ctx, nestedSelection) => ({ + revisions: exportRevisionConnectionHelpers.getQuery( + args, + ctx, + nestedSelection + ), + }), + resolve: (model, args, ctx) => + exportRevisionConnectionHelpers.resolve(model.revisions, args, ctx), + }), }), }); @@ -110,3 +125,34 @@ export const modelConnectionHelpers = prismaConnectionHelpers( "Model", { cursor: "id" } ); + +const exportRevisionConnectionHelpers = prismaConnectionHelpers( + builder, + "ModelRevision", + { + cursor: "id", + args: (t) => ({ + variableId: t.string({ required: true }), + }), + select: (nodeSelection, args) => ({ + exports: nodeSelection({ + where: { + variableName: args.variableId, + }, + }), + }), + query: (args) => ({ + where: { + exports: { + some: { + variableName: args.variableId, + }, + }, + }, + orderBy: { + createdAt: "desc" as const, + }, + }), + resolveNode: (revision) => revision.exports[0], + } +); diff --git a/packages/hub/src/graphql/types/ModelExport.ts b/packages/hub/src/graphql/types/ModelExport.ts new file mode 100644 index 0000000000..5d75575f20 --- /dev/null +++ b/packages/hub/src/graphql/types/ModelExport.ts @@ -0,0 +1,17 @@ +import { builder } from "@/graphql/builder"; + +export const ModelExport = builder.prismaNode("ModelExport", { + id: { field: "id" }, + fields: (t) => ({ + modelRevision: t.relation("modelRevision"), + variableName: t.exposeString("variableName"), + variableType: t.exposeString("variableType"), + docstring: t.exposeString("docstring"), + title: t.exposeString("title", { nullable: true }), + }), +}); + +export const ModelExportConnection = builder.connectionObject({ + type: ModelExport, + name: "ModelExportConnection", +}); diff --git a/packages/hub/src/graphql/types/ModelRevision.ts b/packages/hub/src/graphql/types/ModelRevision.ts index 7dc819912f..b1babe5781 100644 --- a/packages/hub/src/graphql/types/ModelRevision.ts +++ b/packages/hub/src/graphql/types/ModelRevision.ts @@ -1,38 +1,29 @@ +import { UnionRef } from "@pothos/core"; + import { builder } from "@/graphql/builder"; import { prisma } from "@/prisma"; import { NotFoundError } from "../errors/NotFoundError"; import { RelativeValuesExport } from "./RelativeValuesExport"; - -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 }), - }), -}); +import { SquiggleSnippet } from "./SquiggleSnippet"; // TODO - turn into interface? -export const ModelContent = builder.unionType("ModelContent", { +const ModelContent: UnionRef< + { + id: string; + code: string; + version: string; + }, + { + id: string; + code: string; + version: string; + } +> = builder.unionType("ModelContent", { types: [SquiggleSnippet], resolveType: () => SquiggleSnippet, }); -builder.prismaNode("ModelExport", { - id: { field: "id" }, - fields: (t) => ({ - modelRevision: t.relation("modelRevision"), - variableName: t.exposeString("variableName"), - variableType: t.exposeString("variableType"), - docstring: t.exposeString("docstring"), - title: t.exposeString("title", { nullable: true }), - }), -}); - export const ModelRevision = builder.prismaNode("ModelRevision", { id: { field: "id" }, fields: (t) => ({ diff --git a/packages/hub/src/graphql/types/SquiggleSnippet.ts b/packages/hub/src/graphql/types/SquiggleSnippet.ts new file mode 100644 index 0000000000..c2b2cc49da --- /dev/null +++ b/packages/hub/src/graphql/types/SquiggleSnippet.ts @@ -0,0 +1,13 @@ +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 }), + }), +});