From a567bbf283631f1f4a358158730cf0b10a131f5f Mon Sep 17 00:00:00 2001 From: 1aurend Date: Mon, 23 Feb 2026 17:49:02 -0800 Subject: [PATCH 01/95] [C] Scaffold my submissions + review submissions routes --- .../SubmissionListFragment.graphql.ts | 69 ++++++ .../mySubmissionsQuery.graphql.ts | 216 ++++++++++++++++++ .../__generated__/submissionsQuery.graphql.ts | 216 ++++++++++++++++++ .../SubmissionList/SubmissionList.tsx | 77 +++++++ .../submission/SubmissionList/index.ts | 1 + .../submission/SubmissionList/mockData.ts | 58 +++++ packages/admin/fixtures/app.data.ts | 13 ++ packages/admin/helpers/models.ts | 1 + packages/admin/locales/en.json | 9 +- packages/admin/pages/my-submissions/index.tsx | 44 ++++ packages/admin/pages/submissions/index.tsx | 48 ++++ packages/admin/routes/baseRoutes.ts | 10 + packages/lib/package.json | 2 +- 13 files changed, 761 insertions(+), 3 deletions(-) create mode 100644 packages/admin/__generated__/SubmissionListFragment.graphql.ts create mode 100644 packages/admin/__generated__/mySubmissionsQuery.graphql.ts create mode 100644 packages/admin/__generated__/submissionsQuery.graphql.ts create mode 100644 packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx create mode 100644 packages/admin/components/composed/submission/SubmissionList/index.ts create mode 100644 packages/admin/components/composed/submission/SubmissionList/mockData.ts create mode 100644 packages/admin/pages/my-submissions/index.tsx create mode 100644 packages/admin/pages/submissions/index.tsx diff --git a/packages/admin/__generated__/SubmissionListFragment.graphql.ts b/packages/admin/__generated__/SubmissionListFragment.graphql.ts new file mode 100644 index 000000000..7ee5a83a5 --- /dev/null +++ b/packages/admin/__generated__/SubmissionListFragment.graphql.ts @@ -0,0 +1,69 @@ +/** + * @generated SignedSource<<9bb8539618441b670bcfcad920863c4d>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { Fragment, ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type SubmissionListFragment$data = { + readonly nodes: ReadonlyArray<{ + readonly id: string; + readonly slug: string; + }>; + readonly " $fragmentSpreads": FragmentRefs<"ModelListPageFragment">; + readonly " $fragmentType": "SubmissionListFragment"; +}; +export type SubmissionListFragment$key = { + readonly " $data"?: SubmissionListFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"SubmissionListFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "SubmissionListFragment", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Item", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "slug", + "storageKey": null + } + ], + "storageKey": null + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "ModelListPageFragment" + } + ], + "type": "ItemConnection", + "abstractKey": null +}; + +(node as any).hash = "d8e8ff16a0ac9eee771354eba1663900"; + +export default node; diff --git a/packages/admin/__generated__/mySubmissionsQuery.graphql.ts b/packages/admin/__generated__/mySubmissionsQuery.graphql.ts new file mode 100644 index 000000000..077364115 --- /dev/null +++ b/packages/admin/__generated__/mySubmissionsQuery.graphql.ts @@ -0,0 +1,216 @@ +/** + * @generated SignedSource<<229094f98ecf3121e0e928639bda3f03>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type mySubmissionsQuery$variables = { + page: number; +}; +export type mySubmissionsQuery$data = { + readonly viewer: { + readonly items: { + readonly " $fragmentSpreads": FragmentRefs<"SubmissionListFragment">; + }; + }; +}; +export type mySubmissionsQuery = { + response: mySubmissionsQuery$data; + variables: mySubmissionsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "page" + } +], +v1 = [ + { + "kind": "Variable", + "name": "page", + "variableName": "page" + }, + { + "kind": "Literal", + "name": "perPage", + "value": 20 + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "mySubmissionsQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "ItemConnection", + "kind": "LinkedField", + "name": "items", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "SubmissionListFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "mySubmissionsQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "ItemConnection", + "kind": "LinkedField", + "name": "items", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Item", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "slug", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "page", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "pageCount", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "perPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Paginated", + "abstractKey": "__isPaginated" + } + ], + "storageKey": null + }, + (v2/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "9491a3f25ba56b9430593a37f33dc749", + "id": null, + "metadata": {}, + "name": "mySubmissionsQuery", + "operationKind": "query", + "text": "query mySubmissionsQuery(\n $page: Int!\n) {\n viewer {\n items(page: $page, perPage: 20) {\n ...SubmissionListFragment\n }\n id\n }\n}\n\nfragment ModelListPageFragment on Paginated {\n __isPaginated: __typename\n ...ModelPageCountActionsFragment\n ...ModelPaginationFragment\n}\n\nfragment ModelPageCountActionsFragment on Paginated {\n __isPaginated: __typename\n pageInfo {\n page\n pageCount\n perPage\n hasNextPage\n hasPreviousPage\n totalCount\n }\n}\n\nfragment ModelPaginationFragment on Paginated {\n __isPaginated: __typename\n pageInfo {\n page\n pageCount\n }\n}\n\nfragment SubmissionListFragment on ItemConnection {\n nodes {\n id\n slug\n }\n ...ModelListPageFragment\n}\n" + } +}; +})(); + +(node as any).hash = "c6b9665b8acacfa99ecbcb6d24efffce"; + +export default node; diff --git a/packages/admin/__generated__/submissionsQuery.graphql.ts b/packages/admin/__generated__/submissionsQuery.graphql.ts new file mode 100644 index 000000000..535b3a490 --- /dev/null +++ b/packages/admin/__generated__/submissionsQuery.graphql.ts @@ -0,0 +1,216 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type submissionsQuery$variables = { + page: number; +}; +export type submissionsQuery$data = { + readonly viewer: { + readonly items: { + readonly " $fragmentSpreads": FragmentRefs<"SubmissionListFragment">; + }; + }; +}; +export type submissionsQuery = { + response: submissionsQuery$data; + variables: submissionsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "page" + } +], +v1 = [ + { + "kind": "Variable", + "name": "page", + "variableName": "page" + }, + { + "kind": "Literal", + "name": "perPage", + "value": 20 + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "submissionsQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "ItemConnection", + "kind": "LinkedField", + "name": "items", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "SubmissionListFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "submissionsQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "ItemConnection", + "kind": "LinkedField", + "name": "items", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Item", + "kind": "LinkedField", + "name": "nodes", + "plural": true, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "slug", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "page", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "pageCount", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "perPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Paginated", + "abstractKey": "__isPaginated" + } + ], + "storageKey": null + }, + (v2/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "a6e461a496e3af441d1fcee9c4330651", + "id": null, + "metadata": {}, + "name": "submissionsQuery", + "operationKind": "query", + "text": "query submissionsQuery(\n $page: Int!\n) {\n viewer {\n items(page: $page, perPage: 20) {\n ...SubmissionListFragment\n }\n id\n }\n}\n\nfragment ModelListPageFragment on Paginated {\n __isPaginated: __typename\n ...ModelPageCountActionsFragment\n ...ModelPaginationFragment\n}\n\nfragment ModelPageCountActionsFragment on Paginated {\n __isPaginated: __typename\n pageInfo {\n page\n pageCount\n perPage\n hasNextPage\n hasPreviousPage\n totalCount\n }\n}\n\nfragment ModelPaginationFragment on Paginated {\n __isPaginated: __typename\n pageInfo {\n page\n pageCount\n }\n}\n\nfragment SubmissionListFragment on ItemConnection {\n nodes {\n id\n slug\n }\n ...ModelListPageFragment\n}\n" + } +}; +})(); + +(node as any).hash = "38ea6443a026d7ef0ab245d34bd29e51"; + +export default node; diff --git a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx new file mode 100644 index 000000000..32bbe8646 --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx @@ -0,0 +1,77 @@ +import { graphql, useFragment } from "react-relay"; +import { useTranslation } from "react-i18next"; +import ModelListPage from "components/composed/model/ModelListPage"; +import ModelColumns from "components/composed/model/ModelColumns"; +import type { + SubmissionListFragment$data, + SubmissionListFragment$key, +} from "@/relay/SubmissionListFragment.graphql"; +import { MOCK_SUBMISSIONS, type SubmissionNode } from "./mockData"; +import type { ModelTableActionProps } from "@tanstack/react-table"; + +type Props = { + data?: SubmissionListFragment$key; + header?: string; +}; + +function SubmissionList({ data, header }: Props) { + const connection = useFragment(fragment, data); + + const { t } = useTranslation(); + + /* Replace real nodes with mock data while preserving pagination fragment refs */ + const dataWithMockNodes = connection + ? ({ + ...connection, + nodes: MOCK_SUBMISSIONS, + } as SubmissionListFragment$data) + : undefined; + + const columns = [ + ModelColumns.NameColumn({ + accessor: "title", + enableSorting: false, + }), + ModelColumns.StringColumn({ + id: "status", + header: () => t("lists.status_column"), + }), + ModelColumns.StringColumn({ + id: "collection", + header: () => t("lists.collection_column"), + }), + ModelColumns.UpdatedAtColumn({ + enableSorting: false, + }), + ]; + + const actions = { + handleView: ({ row }: ModelTableActionProps) => + row.original.slug ? `/items/${row.original.slug}` : null, + }; + + return ( + + modelName="submission" + header={header ? t(`nav.${header}_submissions`) : undefined} + columns={columns} + data={dataWithMockNodes} + actions={actions} + showSearch + hideFilters + disableSortBy + /> + ); +} + +const fragment = graphql` + fragment SubmissionListFragment on ItemConnection { + nodes { + id + slug + } + ...ModelListPageFragment + } +`; + +export default SubmissionList; diff --git a/packages/admin/components/composed/submission/SubmissionList/index.ts b/packages/admin/components/composed/submission/SubmissionList/index.ts new file mode 100644 index 000000000..a9fe57066 --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionList/index.ts @@ -0,0 +1 @@ +export { default } from "./SubmissionList"; diff --git a/packages/admin/components/composed/submission/SubmissionList/mockData.ts b/packages/admin/components/composed/submission/SubmissionList/mockData.ts new file mode 100644 index 000000000..a5fcde577 --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionList/mockData.ts @@ -0,0 +1,58 @@ +export interface SubmissionNode { + [key: string]: unknown; + id: string; + slug: string; + title: string; + status: string; + collection: string; + updatedAt: string; + allowedActions: string[]; +} + +export const MOCK_SUBMISSIONS: readonly SubmissionNode[] = [ + { + id: "mock-submission-1", + slug: "mock-submission-1", + title: "Advances in Digital Preservation Techniques", + status: "Draft", + collection: "Digital Humanities Quarterly", + updatedAt: "2026-02-20T14:30:00Z", + allowedActions: [], + }, + { + id: "mock-submission-2", + slug: "mock-submission-2", + title: "Open Access and Community Engagement", + status: "In Review", + collection: "Library Science Research", + updatedAt: "2026-02-18T09:15:00Z", + allowedActions: [], + }, + { + id: "mock-submission-3", + slug: "mock-submission-3", + title: "Metadata Standards for Institutional Repositories", + status: "Published", + collection: "Information Management Journal", + updatedAt: "2026-01-25T16:45:00Z", + allowedActions: [], + }, + { + id: "mock-submission-4", + slug: "mock-submission-4", + title: "Collaborative Cataloging in the Digital Age", + status: "Draft", + collection: "Digital Humanities Quarterly", + updatedAt: "2026-02-22T11:00:00Z", + allowedActions: [], + }, + { + id: "mock-submission-5", + slug: "mock-submission-5", + title: "Linked Data Applications for Cultural Heritage", + status: "In Review", + collection: "Archives & Records", + updatedAt: "2026-02-10T08:30:00Z", + allowedActions: [], + }, +]; diff --git a/packages/admin/fixtures/app.data.ts b/packages/admin/fixtures/app.data.ts index 85f5d762a..2a704a7ac 100644 --- a/packages/admin/fixtures/app.data.ts +++ b/packages/admin/fixtures/app.data.ts @@ -47,6 +47,13 @@ const PERMALINKS_LINK = { model: "permalink", }; +const SUBMISSIONS_LINK = { + label: "glossary.submission_other", + route: "submissions", + actions: "admin.access", + model: "submission", +}; + /** * App data */ @@ -69,12 +76,17 @@ const appData = { CONTRIBUTORS_LINK, HARVESTING_LINK, PERMALINKS_LINK, + SUBMISSIONS_LINK, ], }, ], account: { label: "nav.account_header", children: [ + { + label: "nav.my_submissions", + route: "my-submissions", + }, { label: "nav.profile", route: "profile", @@ -99,6 +111,7 @@ const appData = { CONTRIBUTORS_LINK, HARVESTING_LINK, PERMALINKS_LINK, + SUBMISSIONS_LINK, ], }, ], diff --git a/packages/admin/helpers/models.ts b/packages/admin/helpers/models.ts index 459a2609b..b59ffe56a 100644 --- a/packages/admin/helpers/models.ts +++ b/packages/admin/helpers/models.ts @@ -25,6 +25,7 @@ const MODEL_MAP = { MEMBER: "member", ACCESS: "access", PERMALINK: "permalink", + SUBMISSION: "submissions", }; export type ModelNames = keyof typeof MODEL_MAP; diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json index 9aaf28c28..ea19a5780 100644 --- a/packages/admin/locales/en.json +++ b/packages/admin/locales/en.json @@ -94,7 +94,9 @@ "member": "member", "member_other": "members", "metadata": "metadata", - "metadata_mapping": "metadata mapping" + "metadata_mapping": "metadata mapping", + "submission": "submission", + "submission_other": "submissions" }, "lists": { "admin_column": "Admin?", @@ -144,6 +146,7 @@ "began_at_column": "Latest Event", "ended_at_column": "Ended At", "status_column": "Status", + "collection_column": "Collection", "identifier_column": "Identifier", "entity_count_column": "Entity Count", "has_errors_column": "Has Errors", @@ -193,7 +196,9 @@ "harvest_mapping_new": "New Harvest Mapping", "metadata_mappings": "Metadata Mappings", "new_metadata_mapping": "Metadata Mapping", - "permalinks": "Permalinks" + "permalinks": "Permalinks", + "my_submissions": "My Submissions", + "review_submissions": "Review Submissions" }, "messages": { "marked_for_purge": "Marked for purge", diff --git a/packages/admin/pages/my-submissions/index.tsx b/packages/admin/pages/my-submissions/index.tsx new file mode 100644 index 000000000..2ae170efc --- /dev/null +++ b/packages/admin/pages/my-submissions/index.tsx @@ -0,0 +1,44 @@ +import { usePreloadedQuery, graphql, PreloadedQuery } from "react-relay"; +import { QueryTransitionWrapper } from "@wdp/lib/api/components"; +import { useBaseListQueryVars } from "hooks"; +import { LoadingPage } from "components/atomic/loading"; +import SubmissionList from "components/composed/submission/SubmissionList"; +import HtmlHead from "components/global/HtmlHead"; +import { mySubmissionsQuery as Query } from "@/relay/mySubmissionsQuery.graphql"; + +export default function MySubmissions() { + const { page } = useBaseListQueryVars(); + + return ( + <> + + + query={query} + variables={{ page }} + loadingFallback={} + > + {({ queryRef }) => + queryRef ? : + } + + + ); +} + +const ListQuery = ({ queryRef }: { queryRef: PreloadedQuery }) => { + const { + viewer: { items }, + } = usePreloadedQuery(query, queryRef); + + return ; +}; + +const query = graphql` + query mySubmissionsQuery($page: Int!) { + viewer { + items(page: $page, perPage: 20) { + ...SubmissionListFragment + } + } + } +`; diff --git a/packages/admin/pages/submissions/index.tsx b/packages/admin/pages/submissions/index.tsx new file mode 100644 index 000000000..02c4a9d32 --- /dev/null +++ b/packages/admin/pages/submissions/index.tsx @@ -0,0 +1,48 @@ +import { usePreloadedQuery, graphql, PreloadedQuery } from "react-relay"; +import { QueryTransitionWrapper } from "@wdp/lib/api/components"; +import { useBaseListQueryVars } from "hooks"; +import { LoadingPage } from "components/atomic/loading"; +import SubmissionList from "components/composed/submission/SubmissionList"; +import HtmlHead from "components/global/HtmlHead"; +import { submissionsQuery as Query } from "@/relay/submissionsQuery.graphql"; + +export default function Submissions() { + const { page } = useBaseListQueryVars(); + + return ( + <> + + + query={query} + variables={{ page }} + loadingFallback={} + > + {({ queryRef }) => + queryRef ? ( + + ) : ( + + ) + } + + + ); +} + +const ListQuery = ({ queryRef }: { queryRef: PreloadedQuery }) => { + const { + viewer: { items }, + } = usePreloadedQuery(query, queryRef); + + return ; +}; + +const query = graphql` + query submissionsQuery($page: Int!) { + viewer { + items(page: $page, perPage: 20) { + ...SubmissionListFragment + } + } + } +`; diff --git a/packages/admin/routes/baseRoutes.ts b/packages/admin/routes/baseRoutes.ts index bff12500d..88b3ce5b5 100644 --- a/packages/admin/routes/baseRoutes.ts +++ b/packages/admin/routes/baseRoutes.ts @@ -253,6 +253,16 @@ export const baseRoutes: BaseRoute[] = [ }, ], }, + { + name: "my-submissions", + path: "/my-submissions", + label: "glossary.submission_other", + }, + { + name: "submissions", + path: "/submissions", + label: "glossary.submission_other", + }, { name: "communities.search", path: "/communities/search", diff --git a/packages/lib/package.json b/packages/lib/package.json index 660a48661..1ff1ec194 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -6,7 +6,7 @@ "scripts": { "lint": "eslint --cache \"**/*.{ts,tsx}\"", "fix": "eslint --cache --fix \"**/*.{ts,tsx}\"", - "types": "npx tsc --strict -w", + "types": "npx tsc --strict", "depcheck": "depcheck" }, "author": "", From 53cef32a5e0108b975ca2c088c0896e2b1685cb1 Mon Sep 17 00:00:00 2001 From: 1aurend Date: Mon, 23 Feb 2026 18:36:34 -0800 Subject: [PATCH 02/95] [C] Scaffold new submission form --- .../SubmissionCreateFormMutation.graphql.ts | 250 +++++++++++++++++ .../newSubmissionQuery.graphql.ts | 126 +++++++++ .../SubmissionCreateForm.tsx | 252 ++++++++++++++++++ .../submission/SubmissionCreateForm/index.ts | 1 + .../SubmissionList/SubmissionList.tsx | 10 + packages/admin/locales/en.json | 14 +- packages/admin/pages/my-submissions/new.tsx | 41 +++ packages/admin/routes/baseRoutes.ts | 7 + 8 files changed, 697 insertions(+), 4 deletions(-) create mode 100644 packages/admin/__generated__/SubmissionCreateFormMutation.graphql.ts create mode 100644 packages/admin/__generated__/newSubmissionQuery.graphql.ts create mode 100644 packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx create mode 100644 packages/admin/components/composed/submission/SubmissionCreateForm/index.ts create mode 100644 packages/admin/pages/my-submissions/new.tsx diff --git a/packages/admin/__generated__/SubmissionCreateFormMutation.graphql.ts b/packages/admin/__generated__/SubmissionCreateFormMutation.graphql.ts new file mode 100644 index 000000000..399c48eb2 --- /dev/null +++ b/packages/admin/__generated__/SubmissionCreateFormMutation.graphql.ts @@ -0,0 +1,250 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Mutation } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type DatePrecision = "DAY" | "MONTH" | "NONE" | "YEAR" | "%future added value"; +export type EntityVisibility = "HIDDEN" | "LIMITED" | "VISIBLE" | "%future added value"; +export type UploadStorage = "CACHE" | "%future added value"; +export type CreateItemInput = { + clientMutationId?: string | null | undefined; + doi?: string | null | undefined; + heroImage?: UploadedFileInput | null | undefined; + heroImageMetadata?: ImageMetadataInput | null | undefined; + parentId: string; + published?: VariablePrecisionDateInput | null | undefined; + schemaVersionSlug?: string | null | undefined; + subtitle?: string | null | undefined; + summary?: string | null | undefined; + thumbnail?: UploadedFileInput | null | undefined; + thumbnailMetadata?: ImageMetadataInput | null | undefined; + title: string; + visibility: EntityVisibility; + visibleAfterAt?: string | null | undefined; + visibleUntilAt?: string | null | undefined; +}; +export type UploadedFileInput = { + id: any; + metadata?: UploadedFileMetadataInput | null | undefined; + storage?: UploadStorage | null | undefined; +}; +export type UploadedFileMetadataInput = { + alt?: string | null | undefined; + filename?: string | null | undefined; + mimeType?: string | null | undefined; +}; +export type ImageMetadataInput = { + alt?: string | null | undefined; +}; +export type VariablePrecisionDateInput = { + precision: DatePrecision; + value?: string | null | undefined; +}; +export type SubmissionCreateFormMutation$variables = { + input: CreateItemInput; +}; +export type SubmissionCreateFormMutation$data = { + readonly createItem: { + readonly item: { + readonly slug: string; + readonly title: string; + } | null | undefined; + readonly " $fragmentSpreads": FragmentRefs<"MutationForm_mutationErrors">; + } | null | undefined; +}; +export type SubmissionCreateFormMutation = { + response: SubmissionCreateFormMutation$data; + variables: SubmissionCreateFormMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "slug", + "storageKey": null +}, +v4 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MutationAttributeError", + "kind": "LinkedField", + "name": "attributeErrors", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "path", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "type", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "messages", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "MutationGlobalError", + "kind": "LinkedField", + "name": "globalErrors", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "message", + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "StandardMutationPayload", + "abstractKey": "__isStandardMutationPayload" +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SubmissionCreateFormMutation", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "CreateItemPayload", + "kind": "LinkedField", + "name": "createItem", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Item", + "kind": "LinkedField", + "name": "item", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + }, + { + "kind": "InlineDataFragmentSpread", + "name": "MutationForm_mutationErrors", + "selections": [ + (v4/*: any*/) + ], + "args": null, + "argumentDefinitions": [] + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SubmissionCreateFormMutation", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "CreateItemPayload", + "kind": "LinkedField", + "name": "createItem", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Item", + "kind": "LinkedField", + "name": "item", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + }, + (v4/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "8f9e0ee4dde4eb6721aabccbc72a938f", + "id": null, + "metadata": {}, + "name": "SubmissionCreateFormMutation", + "operationKind": "mutation", + "text": "mutation SubmissionCreateFormMutation(\n $input: CreateItemInput!\n) {\n createItem(input: $input) {\n item {\n title\n slug\n id\n }\n ...MutationForm_mutationErrors\n }\n}\n\nfragment MutationForm_mutationErrors on StandardMutationPayload {\n __isStandardMutationPayload: __typename\n attributeErrors {\n path\n type\n messages\n }\n globalErrors {\n message\n }\n}\n" + } +}; +})(); + +(node as any).hash = "e17e47b342b08636f83d69c9c80e54ab"; + +export default node; diff --git a/packages/admin/__generated__/newSubmissionQuery.graphql.ts b/packages/admin/__generated__/newSubmissionQuery.graphql.ts new file mode 100644 index 000000000..5d683ff2b --- /dev/null +++ b/packages/admin/__generated__/newSubmissionQuery.graphql.ts @@ -0,0 +1,126 @@ +/** + * @generated SignedSource<<25c2b68e1d9dc41500cd5b4f575aa21e>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type SchemaKind = "COLLECTION" | "COMMUNITY" | "ITEM" | "%future added value"; +export type newSubmissionQuery$variables = { + schemaKind: SchemaKind; +}; +export type newSubmissionQuery$data = { + readonly " $fragmentSpreads": FragmentRefs<"SchemaSelectFragment">; +}; +export type newSubmissionQuery = { + response: newSubmissionQuery$data; + variables: newSubmissionQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "schemaKind" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "newSubmissionQuery", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "SchemaSelectFragment" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "newSubmissionQuery", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "kind", + "variableName": "schemaKind" + } + ], + "concreteType": "SchemaVersionOption", + "kind": "LinkedField", + "name": "schemaVersionOptions", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "label", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "value", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "SchemaDefinition", + "kind": "LinkedField", + "name": "schemaDefinition", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "slug", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "c6d3deeff48fd3577ce979aa8ab61689", + "id": null, + "metadata": {}, + "name": "newSubmissionQuery", + "operationKind": "query", + "text": "query newSubmissionQuery(\n $schemaKind: SchemaKind!\n) {\n ...SchemaSelectFragment\n}\n\nfragment SchemaSelectFragment on Query {\n schemaVersionOptions(kind: $schemaKind) {\n label\n value\n schemaDefinition {\n slug\n id\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "d5804e429d22879a53aa11594eadd350"; + +export default node; diff --git a/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx b/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx new file mode 100644 index 000000000..15df295e0 --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx @@ -0,0 +1,252 @@ +import { useCallback, type CSSProperties } from "react"; +import { useRouter } from "next/router"; +import { graphql } from "react-relay"; +import { useFieldArray } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import MutationForm, { + useRenderForm, + useToVariables, + Forms, + useOnSuccess, +} from "components/api/MutationForm"; +import { ButtonControl } from "components/atomic"; +import { Fieldset } from "components/forms"; +import { RouteHelper } from "routes"; +import useIsMobile from "hooks/useIsMobile"; +import type { SchemaSelectFragment$key } from "@/relay/SchemaSelectFragment.graphql"; +import type { + SubmissionCreateFormMutation, + CreateItemInput, +} from "@/relay/SubmissionCreateFormMutation.graphql"; +import type { Control, UseFormRegister } from "react-hook-form"; + +type Fields = Omit & { + contributors: { contributorId: string }[]; + files: { file: string; caption: string }[]; +}; + +type Props = { + data: SchemaSelectFragment$key; +}; + +export default function SubmissionCreateForm({ data }: Props) { + const { t } = useTranslation(); + const router = useRouter(); + + const redirect = useCallback( + (routeName: string) => { + const newRoute = RouteHelper.findRouteByName(routeName); + if (newRoute) router.push({ pathname: newRoute.path }); + }, + [router] + ); + + const onSuccess = useOnSuccess(() => { + redirect("my-submissions"); + }, [redirect]); + + const toVariables = useToVariables( + (data) => ({ + input: { + title: data.title, + subtitle: data.subtitle, + parentId: data.parentId, + schemaVersionSlug: data.schemaVersionSlug, + summary: data.summary, + thumbnail: data.thumbnail, + heroImage: data.heroImage, + published: data.published, + }, + }), + [] + ); + + const defaultValues = { + contributors: [] as Fields["contributors"], + files: [] as Fields["files"], + }; + + const renderForm = useRenderForm( + ({ form: { register, control } }) => ( + + + + + +
+ {t("forms.fields.schema_fields_placeholder")} +
+ + + + + +
+ ), + [] + ); + + return ( + + mutation={mutation} + onSuccess={onSuccess} + onCancel={() => redirect("my-submissions")} + successNotification="messages.create.submission_success" + name="createItem" + refetchTags={["items"]} + toVariables={toVariables} + defaultValues={defaultValues} + > + {renderForm} + + ); +} + +function ContributorsFieldArray({ control }: { control: Control }) { + const { t } = useTranslation(); + const isMobile = useIsMobile(); + const { fields, append, remove } = useFieldArray({ + control, + name: "contributors", + }); + + return ( +
+ {fields.map((field, index) => ( + + + + + + remove(index)} + icon="delete" + size={!isMobile ? "large" : undefined} + > + {isMobile ? t("common.delete") : null} + + + + ))} + append({ contributorId: "" })} + icon="plus" + size={!isMobile ? "large" : undefined} + > + {t("actions.add.contributor_row")} + +
+ ); +} + +function FilesFieldArray({ + control, + register, +}: { + control: Control; + register: UseFormRegister; +}) { + const { t } = useTranslation(); + const isMobile = useIsMobile(); + const { fields, append, remove } = useFieldArray({ + control, + name: "files", + }); + + return ( +
+ {fields.map((field, index) => ( + + + + + + + + + remove(index)} + icon="delete" + size={!isMobile ? "large" : undefined} + > + {isMobile ? t("common.delete") : null} + + + + ))} + append({ file: "", caption: "" })} + icon="plus" + size={!isMobile ? "large" : undefined} + > + {t("actions.add.file_row")} + +
+ ); +} + +const mutation = graphql` + mutation SubmissionCreateFormMutation($input: CreateItemInput!) { + createItem(input: $input) { + item { + title + slug + } + ...MutationForm_mutationErrors + } + } +`; diff --git a/packages/admin/components/composed/submission/SubmissionCreateForm/index.ts b/packages/admin/components/composed/submission/SubmissionCreateForm/index.ts new file mode 100644 index 000000000..de6811eb7 --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionCreateForm/index.ts @@ -0,0 +1 @@ +export { default } from "./SubmissionCreateForm"; diff --git a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx index 32bbe8646..89a3f95cc 100644 --- a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx +++ b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx @@ -2,6 +2,7 @@ import { graphql, useFragment } from "react-relay"; import { useTranslation } from "react-i18next"; import ModelListPage from "components/composed/model/ModelListPage"; import ModelColumns from "components/composed/model/ModelColumns"; +import { ButtonControlGroup, ButtonControlRoute } from "components/atomic"; import type { SubmissionListFragment$data, SubmissionListFragment$key, @@ -50,6 +51,14 @@ function SubmissionList({ data, header }: Props) { row.original.slug ? `/items/${row.original.slug}` : null, }; + const buttons = ( + + + {t("actions.add.submission")} + + + ); + return ( modelName="submission" @@ -57,6 +66,7 @@ function SubmissionList({ data, header }: Props) { columns={columns} data={dataWithMockNodes} actions={actions} + buttons={buttons} showSearch hideFilters disableSortBy diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json index ea19a5780..a140f3ab1 100644 --- a/packages/admin/locales/en.json +++ b/packages/admin/locales/en.json @@ -198,7 +198,8 @@ "new_metadata_mapping": "Metadata Mapping", "permalinks": "Permalinks", "my_submissions": "My Submissions", - "review_submissions": "Review Submissions" + "review_submissions": "Review Submissions", + "new_submission": "New Submission" }, "messages": { "marked_for_purge": "Marked for purge", @@ -273,7 +274,8 @@ "harvest_source_success": "Harvest source created successfully.", "harvest_mapping_success": "Harvest mapping created successfully", "harvest_attempt_success": "Harvest attempt started successfully", - "harvest_metadata_mapping_success": "Harvest metadata mapping created successfully" + "harvest_metadata_mapping_success": "Harvest metadata mapping created successfully", + "submission_success": "Submission created successfully." }, "delete": { "confirm_label": "Confirm Delete", @@ -347,7 +349,10 @@ "harvest_mapping": "$t(glossary.harvest_mapping)", "harvest_mapping_header": "Add Harvest Mapping", "harvest_attempt": "$t(glossary.harvest_attempt)", - "harvest_metadata_mapping": "$t(glossary.metadata_mapping)" + "harvest_metadata_mapping": "$t(glossary.metadata_mapping)", + "submission": "New Submission", + "contributor_row": "Add Contributor", + "file_row": "Add File" }, "create": { "contributor_person": "Create Person", @@ -563,7 +568,8 @@ "max_records_description": "Default 80,000", "field": "Field", "pattern": "Pattern", - "pattern_description": "An ANSI-compatible regular expression to match against" + "pattern_description": "An ANSI-compatible regular expression to match against", + "schema_fields_placeholder": "Schema-specific fields will appear here after this feature is fully connected." }, "validation": { "year": "Must be a four digit year", diff --git a/packages/admin/pages/my-submissions/new.tsx b/packages/admin/pages/my-submissions/new.tsx new file mode 100644 index 000000000..5383837b3 --- /dev/null +++ b/packages/admin/pages/my-submissions/new.tsx @@ -0,0 +1,41 @@ +import { usePreloadedQuery, graphql, PreloadedQuery } from "react-relay"; +import { useTranslation } from "react-i18next"; +import { QueryTransitionWrapper } from "@wdp/lib/api/components"; +import { LoadingPage } from "components/atomic/loading"; +import { PageHeader, BackToAll } from "components/layout"; +import HtmlHead from "components/global/HtmlHead"; +import SubmissionCreateForm from "components/composed/submission/SubmissionCreateForm"; +import type { newSubmissionQuery as Query } from "@/relay/newSubmissionQuery.graphql"; + +export default function NewSubmission() { + const { t } = useTranslation(); + + return ( + <> + + + + + query={query} + variables={{ schemaKind: "ITEM" }} + loadingFallback={} + > + {({ queryRef }) => + queryRef ? : null + } + + + ); +} + +const FormWithData = ({ queryRef }: { queryRef: PreloadedQuery }) => { + const data = usePreloadedQuery(query, queryRef); + + return ; +}; + +const query = graphql` + query newSubmissionQuery($schemaKind: SchemaKind!) { + ...SchemaSelectFragment + } +`; diff --git a/packages/admin/routes/baseRoutes.ts b/packages/admin/routes/baseRoutes.ts index 88b3ce5b5..426068f9e 100644 --- a/packages/admin/routes/baseRoutes.ts +++ b/packages/admin/routes/baseRoutes.ts @@ -257,6 +257,13 @@ export const baseRoutes: BaseRoute[] = [ name: "my-submissions", path: "/my-submissions", label: "glossary.submission_other", + routes: [ + { + name: "my-submissions.new", + path: "/my-submissions/new", + label: "nav.new_submission", + }, + ], }, { name: "submissions", From f986a753b9273c25a013a1431be21a9e08449ddf Mon Sep 17 00:00:00 2001 From: 1aurend Date: Tue, 24 Feb 2026 08:58:32 -0800 Subject: [PATCH 03/95] [C] Scaffold submission detail routes --- .../SubmissionLayoutFragment.graphql.ts | 58 ++++++++++ .../SubmissionCreateForm.tsx | 12 ++- .../SubmissionLayout/SubmissionLayout.tsx | 48 +++++++++ .../submission/SubmissionLayout/index.ts | 1 + .../SubmissionList/SubmissionList.tsx | 15 ++- packages/admin/locales/en.json | 3 +- .../pages/my-submissions/[slug]/_layout.tsx | 100 ++++++++++++++++++ .../pages/my-submissions/[slug]/details.tsx | 34 ++++++ .../pages/my-submissions/[slug]/history.tsx | 34 ++++++ .../pages/my-submissions/[slug]/index.tsx | 8 ++ .../pages/submissions/[slug]/_layout.tsx | 39 +++++++ .../pages/submissions/[slug]/details.tsx | 19 ++++ .../pages/submissions/[slug]/history.tsx | 19 ++++ .../admin/pages/submissions/[slug]/index.tsx | 8 ++ packages/admin/pages/submissions/index.tsx | 8 +- packages/admin/routes/baseRoutes.ts | 38 +++++++ 16 files changed, 428 insertions(+), 16 deletions(-) create mode 100644 packages/admin/__generated__/SubmissionLayoutFragment.graphql.ts create mode 100644 packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx create mode 100644 packages/admin/components/composed/submission/SubmissionLayout/index.ts create mode 100644 packages/admin/pages/my-submissions/[slug]/_layout.tsx create mode 100644 packages/admin/pages/my-submissions/[slug]/details.tsx create mode 100644 packages/admin/pages/my-submissions/[slug]/history.tsx create mode 100644 packages/admin/pages/my-submissions/[slug]/index.tsx create mode 100644 packages/admin/pages/submissions/[slug]/_layout.tsx create mode 100644 packages/admin/pages/submissions/[slug]/details.tsx create mode 100644 packages/admin/pages/submissions/[slug]/history.tsx create mode 100644 packages/admin/pages/submissions/[slug]/index.tsx diff --git a/packages/admin/__generated__/SubmissionLayoutFragment.graphql.ts b/packages/admin/__generated__/SubmissionLayoutFragment.graphql.ts new file mode 100644 index 000000000..72e6f77ab --- /dev/null +++ b/packages/admin/__generated__/SubmissionLayoutFragment.graphql.ts @@ -0,0 +1,58 @@ +/** + * @generated SignedSource<<12be081833a177b0b3007657eeb20133>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { Fragment, ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type SubmissionLayoutFragment$data = { + readonly __typename: "Item"; + readonly slug: string; + readonly title: string; + readonly " $fragmentType": "SubmissionLayoutFragment"; +}; +export type SubmissionLayoutFragment$key = { + readonly " $data"?: SubmissionLayoutFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"SubmissionLayoutFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "SubmissionLayoutFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "slug", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + } + ], + "type": "Item", + "abstractKey": null +}; + +(node as any).hash = "f5a8320104a4ff90b3599b2a92b6b308"; + +export default node; diff --git a/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx b/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx index 15df295e0..0a879a956 100644 --- a/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx +++ b/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx @@ -38,7 +38,7 @@ export default function SubmissionCreateForm({ data }: Props) { const newRoute = RouteHelper.findRouteByName(routeName); if (newRoute) router.push({ pathname: newRoute.path }); }, - [router] + [router], ); const onSuccess = useOnSuccess(() => { @@ -52,16 +52,18 @@ export default function SubmissionCreateForm({ data }: Props) { subtitle: data.subtitle, parentId: data.parentId, schemaVersionSlug: data.schemaVersionSlug, + visibility: data.visibility, summary: data.summary, thumbnail: data.thumbnail, heroImage: data.heroImage, published: data.published, }, }), - [] + [], ); const defaultValues = { + visibility: "VISIBLE" as Fields["visibility"], contributors: [] as Fields["contributors"], files: [] as Fields["files"], }; @@ -96,8 +98,8 @@ export default function SubmissionCreateForm({ data }: Props) { className="t-copy-sm a-color-light" style={ { - "flexBasis": "var(--form-grid-item-width-wide)", - "paddingBlock": "var(--form-grid-row-gap)", + flexBasis: "var(--form-grid-item-width-wide)", + paddingBlock: "var(--form-grid-row-gap)", } as CSSProperties } > @@ -121,7 +123,7 @@ export default function SubmissionCreateForm({ data }: Props) { ), - [] + [], ); return ( diff --git a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx new file mode 100644 index 000000000..4d62db41b --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx @@ -0,0 +1,48 @@ +import { graphql } from "react-relay"; +import { useChildRouteLinks, useMaybeFragment, useRouteSlug } from "hooks"; +import { PageHeader, BackToAll } from "components/layout"; +import HtmlHead from "components/global/HtmlHead"; +import { MOCK_SUBMISSIONS } from "components/composed/submission/SubmissionList/mockData"; +import type { SubmissionLayoutFragment$key } from "@/relay/SubmissionLayoutFragment.graphql"; + +export default function SubmissionLayout({ + children, + data, + slug: slugProp, + parentRoute = "my-submissions", + detailRoute = "my-submissions.detail", +}: { + children: React.ReactNode; + data?: SubmissionLayoutFragment$key | null; + slug?: string; + parentRoute?: string; + detailRoute?: string; +}) { + const routeSlug = useRouteSlug() || undefined; + const slug = slugProp || routeSlug; + const tabRoutes = useChildRouteLinks(detailRoute, { slug }); + const item = useMaybeFragment(fragment, data); + + // Fall back to mock data title when no API data is available + const title = + item?.title || MOCK_SUBMISSIONS.find((s) => s.slug === slug)?.title; + + return ( + <> + +
+ + + {children} +
+ + ); +} + +const fragment = graphql` + fragment SubmissionLayoutFragment on Item { + __typename + slug + title + } +`; diff --git a/packages/admin/components/composed/submission/SubmissionLayout/index.ts b/packages/admin/components/composed/submission/SubmissionLayout/index.ts new file mode 100644 index 000000000..62b886864 --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionLayout/index.ts @@ -0,0 +1 @@ +export { default } from "./SubmissionLayout"; diff --git a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx index 89a3f95cc..b468942a4 100644 --- a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx +++ b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx @@ -1,4 +1,5 @@ import { graphql, useFragment } from "react-relay"; +import { useRouter } from "next/router"; import { useTranslation } from "react-i18next"; import ModelListPage from "components/composed/model/ModelListPage"; import ModelColumns from "components/composed/model/ModelColumns"; @@ -12,13 +13,13 @@ import type { ModelTableActionProps } from "@tanstack/react-table"; type Props = { data?: SubmissionListFragment$key; - header?: string; }; -function SubmissionList({ data, header }: Props) { +function SubmissionList({ data }: Props) { const connection = useFragment(fragment, data); const { t } = useTranslation(); + const router = useRouter(); /* Replace real nodes with mock data while preserving pagination fragment refs */ const dataWithMockNodes = connection @@ -28,9 +29,13 @@ function SubmissionList({ data, header }: Props) { } as SubmissionListFragment$data) : undefined; + const isReview = router.pathname.startsWith("/submissions"); + const detailRoute = isReview ? "submissions.detail" : "my-submissions.detail"; + const columns = [ ModelColumns.NameColumn({ accessor: "title", + route: detailRoute, enableSorting: false, }), ModelColumns.StringColumn({ @@ -48,7 +53,9 @@ function SubmissionList({ data, header }: Props) { const actions = { handleView: ({ row }: ModelTableActionProps) => - row.original.slug ? `/items/${row.original.slug}` : null, + row.original.slug + ? `/${isReview ? "submissions" : "my-submissions"}/${row.original.slug}` + : null, }; const buttons = ( @@ -62,7 +69,7 @@ function SubmissionList({ data, header }: Props) { return ( modelName="submission" - header={header ? t(`nav.${header}_submissions`) : undefined} + header={isReview ? t("nav.review_submissions") : t("nav.my_submissions")} columns={columns} data={dataWithMockNodes} actions={actions} diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json index a140f3ab1..9d1ac3bb4 100644 --- a/packages/admin/locales/en.json +++ b/packages/admin/locales/en.json @@ -199,7 +199,8 @@ "permalinks": "Permalinks", "my_submissions": "My Submissions", "review_submissions": "Review Submissions", - "new_submission": "New Submission" + "new_submission": "New Submission", + "history": "History" }, "messages": { "marked_for_purge": "Marked for purge", diff --git a/packages/admin/pages/my-submissions/[slug]/_layout.tsx b/packages/admin/pages/my-submissions/[slug]/_layout.tsx new file mode 100644 index 000000000..2fcb5d290 --- /dev/null +++ b/packages/admin/pages/my-submissions/[slug]/_layout.tsx @@ -0,0 +1,100 @@ +import { ComponentType } from "react"; +import { + PreloadedQuery, + GraphQLTaggedNode, + // graphql, + // useLazyLoadQuery, +} from "react-relay"; +import { OperationType } from "relay-runtime"; +// import { useSearchParams } from "next/navigation"; +// import { QueryTransitionWrapper } from "@wdp/lib/api/components"; +import { ModelListProps } from "components/composed/model/ModelList"; +import { useRouteSlug } from "hooks"; +import SubmissionLayout from "components/composed/submission/SubmissionLayout"; +import ErrorPage from "next/error"; +// import { LoadingCircle } from "components/atomic"; +// import ModelListPageSkeleton from "components/composed/model/ModelListPageSkeleton"; +// import type { LayoutSubmissionQuery as LayoutQuery } from "@/relay/LayoutSubmissionQuery.graphql"; + +export default function Layout(props: Props) { + const slug = useRouteSlug() as string; + + if (!slug) return ; + + // TODO: Uncomment when real submission API is available + // const queryVars = useBaseListQueryVars(); + // const searchParams = useSearchParams(); + // + // const { item } = useLazyLoadQuery( + // submissionQuery, + // { slug }, + // { fetchPolicy: "store-or-network" }, + // ); + // + // if (!item) return ; + // + // const searchVars: Record = {}; + // searchParams.forEach((value, key) => (searchVars[key] = value)); + + const { PageComponent, pageComponentProps } = props; + + return ( + + + + ); + + // TODO: Uncomment when real submission API is available + // return ( + // + // + // query={query} + // variables={{ + // ...defaultQueryVars, + // ...searchVars, + // ...queryVars, + // slug, + // }} + // loadingFallback={} + // refetchTags={refetchTags} + // > + // {({ queryRef }) => + // queryRef ? ( + // + // ) : showLoadingCircle ? ( + // + // ) : ( + // + // ) + // } + // + // + // ); +} + +type Props = { + pageComponentProps: PageProps; + PageComponent: ComponentType>; + query?: GraphQLTaggedNode | null; + refetchTags?: string[]; + showLoadingCircle?: boolean; + modelName?: ModelListProps["modelName"]; // eslint-disable-line @typescript-eslint/no-explicit-any + defaultQueryVars?: Record; +}; + +type PageProps = { + queryRef?: PreloadedQuery; +}; + +// TODO: Uncomment when real submission API is available +// const submissionQuery = graphql` +// query LayoutSubmissionQuery($slug: Slug!) { +// item(slug: $slug) { +// ...SubmissionLayoutFragment +// } +// } +// `; diff --git a/packages/admin/pages/my-submissions/[slug]/details.tsx b/packages/admin/pages/my-submissions/[slug]/details.tsx new file mode 100644 index 000000000..ee0bb8f2a --- /dev/null +++ b/packages/admin/pages/my-submissions/[slug]/details.tsx @@ -0,0 +1,34 @@ +// import { graphql, usePreloadedQuery, PreloadedQuery } from "react-relay"; +// import type { detailsSubmissionQuery as Query } from "@/relay/detailsSubmissionQuery.graphql"; +import Layout from "./_layout"; +import type { GetLayout } from "@wdp/lib/types/page"; + +function SubmissionDetails() { + // TODO: Uncomment when real submission API is available + // const { item } = usePreloadedQuery(query, queryRef); + + return ( +
+ Submission details coming soon. +
+ ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getLayout: GetLayout = (props) => { + return ; +}; + +SubmissionDetails.getLayout = getLayout; + +export default SubmissionDetails; + +// TODO: Uncomment when real submission API is available +// const query = graphql` +// query detailsSubmissionQuery($slug: Slug!) { +// item(slug: $slug) { +// title +// slug +// } +// } +// `; diff --git a/packages/admin/pages/my-submissions/[slug]/history.tsx b/packages/admin/pages/my-submissions/[slug]/history.tsx new file mode 100644 index 000000000..2694de106 --- /dev/null +++ b/packages/admin/pages/my-submissions/[slug]/history.tsx @@ -0,0 +1,34 @@ +// import { graphql, usePreloadedQuery, PreloadedQuery } from "react-relay"; +// import type { historySubmissionQuery as Query } from "@/relay/historySubmissionQuery.graphql"; +import Layout from "./_layout"; +import type { GetLayout } from "@wdp/lib/types/page"; + +function SubmissionHistory() { + // TODO: Uncomment when real submission API is available + // const { item } = usePreloadedQuery(query, queryRef); + + return ( +
+ Submission history coming soon. +
+ ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getLayout: GetLayout = (props) => { + return ; +}; + +SubmissionHistory.getLayout = getLayout; + +export default SubmissionHistory; + +// TODO: Uncomment when real submission API is available +// const query = graphql` +// query historySubmissionQuery($slug: Slug!) { +// item(slug: $slug) { +// title +// slug +// } +// } +// `; diff --git a/packages/admin/pages/my-submissions/[slug]/index.tsx b/packages/admin/pages/my-submissions/[slug]/index.tsx new file mode 100644 index 000000000..18487b6aa --- /dev/null +++ b/packages/admin/pages/my-submissions/[slug]/index.tsx @@ -0,0 +1,8 @@ +import { Page } from "@wdp/lib/types/page"; + +// This is a redirect route. See routes/baseRoutes.ts. +const Submission: Page = () => { + return null; +}; + +export default Submission; diff --git a/packages/admin/pages/submissions/[slug]/_layout.tsx b/packages/admin/pages/submissions/[slug]/_layout.tsx new file mode 100644 index 000000000..ae8786906 --- /dev/null +++ b/packages/admin/pages/submissions/[slug]/_layout.tsx @@ -0,0 +1,39 @@ +import { ComponentType } from "react"; +import { PreloadedQuery, GraphQLTaggedNode } from "react-relay"; +import { OperationType } from "relay-runtime"; +import { ModelListProps } from "components/composed/model/ModelList"; +import { useRouteSlug } from "hooks"; +import SubmissionLayout from "components/composed/submission/SubmissionLayout"; +import ErrorPage from "next/error"; + +export default function Layout(props: Props) { + const slug = useRouteSlug() as string; + + if (!slug) return ; + + const { PageComponent, pageComponentProps } = props; + + return ( + + + + ); +} + +type Props = { + pageComponentProps: PageProps; + PageComponent: ComponentType>; + query?: GraphQLTaggedNode | null; + refetchTags?: string[]; + showLoadingCircle?: boolean; + modelName?: ModelListProps["modelName"]; // eslint-disable-line @typescript-eslint/no-explicit-any + defaultQueryVars?: Record; +}; + +type PageProps = { + queryRef?: PreloadedQuery; +}; diff --git a/packages/admin/pages/submissions/[slug]/details.tsx b/packages/admin/pages/submissions/[slug]/details.tsx new file mode 100644 index 000000000..8a91f930b --- /dev/null +++ b/packages/admin/pages/submissions/[slug]/details.tsx @@ -0,0 +1,19 @@ +import Layout from "./_layout"; +import type { GetLayout } from "@wdp/lib/types/page"; + +function SubmissionDetails() { + return ( +
+ Submission details coming soon. +
+ ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getLayout: GetLayout = (props) => { + return ; +}; + +SubmissionDetails.getLayout = getLayout; + +export default SubmissionDetails; diff --git a/packages/admin/pages/submissions/[slug]/history.tsx b/packages/admin/pages/submissions/[slug]/history.tsx new file mode 100644 index 000000000..545d8db68 --- /dev/null +++ b/packages/admin/pages/submissions/[slug]/history.tsx @@ -0,0 +1,19 @@ +import Layout from "./_layout"; +import type { GetLayout } from "@wdp/lib/types/page"; + +function SubmissionHistory() { + return ( +
+ Submission history coming soon. +
+ ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getLayout: GetLayout = (props) => { + return ; +}; + +SubmissionHistory.getLayout = getLayout; + +export default SubmissionHistory; diff --git a/packages/admin/pages/submissions/[slug]/index.tsx b/packages/admin/pages/submissions/[slug]/index.tsx new file mode 100644 index 000000000..18487b6aa --- /dev/null +++ b/packages/admin/pages/submissions/[slug]/index.tsx @@ -0,0 +1,8 @@ +import { Page } from "@wdp/lib/types/page"; + +// This is a redirect route. See routes/baseRoutes.ts. +const Submission: Page = () => { + return null; +}; + +export default Submission; diff --git a/packages/admin/pages/submissions/index.tsx b/packages/admin/pages/submissions/index.tsx index 02c4a9d32..73aa41b29 100644 --- a/packages/admin/pages/submissions/index.tsx +++ b/packages/admin/pages/submissions/index.tsx @@ -18,11 +18,7 @@ export default function Submissions() { loadingFallback={} > {({ queryRef }) => - queryRef ? ( - - ) : ( - - ) + queryRef ? : } @@ -34,7 +30,7 @@ const ListQuery = ({ queryRef }: { queryRef: PreloadedQuery }) => { viewer: { items }, } = usePreloadedQuery(query, queryRef); - return ; + return ; }; const query = graphql` diff --git a/packages/admin/routes/baseRoutes.ts b/packages/admin/routes/baseRoutes.ts index 426068f9e..652054766 100644 --- a/packages/admin/routes/baseRoutes.ts +++ b/packages/admin/routes/baseRoutes.ts @@ -263,12 +263,50 @@ export const baseRoutes: BaseRoute[] = [ path: "/my-submissions/new", label: "nav.new_submission", }, + { + name: "my-submissions.detail", + path: "/my-submissions/[slug]", + redirect: "/my-submissions/[slug]/details", + label: "glossary.submission", + routes: [ + { + name: "my-submissions.detail.details", + path: "/my-submissions/[slug]/details", + label: "nav.details", + }, + { + name: "my-submissions.detail.history", + path: "/my-submissions/[slug]/history", + label: "nav.history", + }, + ], + }, ], }, { name: "submissions", path: "/submissions", label: "glossary.submission_other", + routes: [ + { + name: "submissions.detail", + path: "/submissions/[slug]", + redirect: "/submissions/[slug]/details", + label: "glossary.submission", + routes: [ + { + name: "submissions.detail.details", + path: "/submissions/[slug]/details", + label: "nav.details", + }, + { + name: "submissions.detail.history", + path: "/submissions/[slug]/history", + label: "nav.history", + }, + ], + }, + ], }, { name: "communities.search", From bff00f56d809ed51c6725bf3ba41036edcbbd9a3 Mon Sep 17 00:00:00 2001 From: 1aurend Date: Tue, 24 Feb 2026 11:30:51 -0800 Subject: [PATCH 04/95] [C] Scaffold review submission drawer --- .../SubmissionLayout/SubmissionLayout.tsx | 22 +++++- .../SubmissionReviewDrawer.tsx | 31 ++++++++ .../SubmissionReviewDrawer/index.ts | 1 + .../SubmissionReviewForm.tsx | 73 +++++++++++++++++++ .../submission/SubmissionReviewForm/index.ts | 1 + packages/admin/hooks/useDrawerHelper.ts | 2 + packages/admin/locales/en.json | 11 ++- 7 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 packages/admin/components/composed/submission/SubmissionReviewDrawer/SubmissionReviewDrawer.tsx create mode 100644 packages/admin/components/composed/submission/SubmissionReviewDrawer/index.ts create mode 100644 packages/admin/components/composed/submission/SubmissionReviewForm/SubmissionReviewForm.tsx create mode 100644 packages/admin/components/composed/submission/SubmissionReviewForm/index.ts diff --git a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx index 4d62db41b..b72c02c84 100644 --- a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx +++ b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx @@ -1,5 +1,7 @@ +import { useTranslation } from "react-i18next"; import { graphql } from "react-relay"; import { useChildRouteLinks, useMaybeFragment, useRouteSlug } from "hooks"; +import { ButtonControlGroup, ButtonControlDrawer } from "components/atomic"; import { PageHeader, BackToAll } from "components/layout"; import HtmlHead from "components/global/HtmlHead"; import { MOCK_SUBMISSIONS } from "components/composed/submission/SubmissionList/mockData"; @@ -18,6 +20,7 @@ export default function SubmissionLayout({ parentRoute?: string; detailRoute?: string; }) { + const { t } = useTranslation(); const routeSlug = useRouteSlug() || undefined; const slug = slugProp || routeSlug; const tabRoutes = useChildRouteLinks(detailRoute, { slug }); @@ -27,12 +30,29 @@ export default function SubmissionLayout({ const title = item?.title || MOCK_SUBMISSIONS.find((s) => s.slug === slug)?.title; + const buttons = parentRoute === "submissions" && ( + + + {t("actions.review_submission")} + + + ); + return ( <>
- + {children}
diff --git a/packages/admin/components/composed/submission/SubmissionReviewDrawer/SubmissionReviewDrawer.tsx b/packages/admin/components/composed/submission/SubmissionReviewDrawer/SubmissionReviewDrawer.tsx new file mode 100644 index 000000000..c63e18a3b --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionReviewDrawer/SubmissionReviewDrawer.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next"; +import Drawer from "components/layout/Drawer"; +import SubmissionReviewForm from "components/composed/submission/SubmissionReviewForm"; +import { MOCK_SUBMISSIONS } from "components/composed/submission/SubmissionList/mockData"; +import type { DialogProps } from "reakit/Dialog"; + +export default function SubmissionReviewDrawer({ + dialog, + params, +}: { + dialog: DialogProps; + params: Record; +}) { + const { t } = useTranslation(); + + // TODO: replace mock lookup with API data + const title = MOCK_SUBMISSIONS.find( + (s) => s.slug === params.drawerSlug, + )?.title; + + return ( + + + + ); +} diff --git a/packages/admin/components/composed/submission/SubmissionReviewDrawer/index.ts b/packages/admin/components/composed/submission/SubmissionReviewDrawer/index.ts new file mode 100644 index 000000000..e1cf7c608 --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionReviewDrawer/index.ts @@ -0,0 +1 @@ +export { default } from "./SubmissionReviewDrawer"; diff --git a/packages/admin/components/composed/submission/SubmissionReviewForm/SubmissionReviewForm.tsx b/packages/admin/components/composed/submission/SubmissionReviewForm/SubmissionReviewForm.tsx new file mode 100644 index 000000000..35b153940 --- /dev/null +++ b/packages/admin/components/composed/submission/SubmissionReviewForm/SubmissionReviewForm.tsx @@ -0,0 +1,73 @@ +import { useForm, FormProvider } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { Grid, Select, Textarea } from "components/forms"; +import { Button } from "components/atomic"; +import * as Styled from "components/api/MutationForm/MutationForm.styles"; + +type Fields = { + decision: string; + commentsForDepositor: string; + internalNotes: string; +}; + +export default function SubmissionReviewForm({ + onSuccess, + onCancel, +}: { + onSuccess?: () => void; + onCancel?: () => void; +}) { + const { t } = useTranslation(); + const form = useForm(); + const { register, handleSubmit } = form; + + const onSubmit = (data: Fields) => { + // TODO: wire up mutation + // eslint-disable-next-line no-console + console.log("Review submission:", data); + onSuccess?.(); + }; + + const decisionOptions = [ + { label: t("forms.fields.decision_approve"), value: "APPROVE" }, + { + label: t("forms.fields.decision_request_revisions"), + value: "REQUEST_REVISIONS", + }, + { label: t("forms.fields.decision_reject"), value: "REJECT" }, + ]; + + return ( + +
+ +