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
+
+
+ {exportRevisions.edges.map(({ node: revision }) => (
+ - changeId(revision.id)}
+ className={clsx(
+ "hover:text-gray-800 cursor-pointer hover:underline text-sm pt-0.5 pb-0.5",
+ revision.id === selected ? "text-blue-900" : "text-gray-400"
+ )}
+ >
+ {format(
+ new Date(revision.modelRevision.createdAtTimestamp),
+ "MMM dd, yyyy"
+ )}
+
+ ))}
+
+ {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 }),
+ }),
+});