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 (
+
+ );
+}
+
+function FilesFieldArray({
+ control,
+ register,
+}: {
+ control: Control;
+ register: UseFormRegister;
+}) {
+ const { t } = useTranslation();
+ const isMobile = useIsMobile();
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: "files",
+ });
+
+ return (
+
+ );
+}
+
+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 (
+ <>
+
+
+ >
+ );
+}
+
+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 (
<>
>
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 (
+
+
+
+ );
+}
diff --git a/packages/admin/components/composed/submission/SubmissionReviewForm/index.ts b/packages/admin/components/composed/submission/SubmissionReviewForm/index.ts
new file mode 100644
index 000000000..066f224fb
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionReviewForm/index.ts
@@ -0,0 +1 @@
+export { default } from "./SubmissionReviewForm";
diff --git a/packages/admin/hooks/useDrawerHelper.ts b/packages/admin/hooks/useDrawerHelper.ts
index ac1699ff3..b6bedaec1 100644
--- a/packages/admin/hooks/useDrawerHelper.ts
+++ b/packages/admin/hooks/useDrawerHelper.ts
@@ -38,6 +38,7 @@ import SearchFilterDrawer from "components/composed/search/SearchFilterDrawer";
import EntityPermalinksAddDrawer from "components/composed/permalinks/EntityPermalinksAddDrawer";
import EntityPermalinksUpdateDrawer from "components/composed/permalinks/EntityPermalinksUpdateDrawer";
import InstancePermalinksAddDrawer from "components/composed/permalinks/InstancePermalinksAddDrawer";
+import SubmissionReviewDrawer from "components/composed/submission/SubmissionReviewDrawer";
const drawerRegistry = {
addPerson: ContributorCreatePersonDrawer,
@@ -75,6 +76,7 @@ const drawerRegistry = {
addPermalink: EntityPermalinksAddDrawer,
editPermalink: EntityPermalinksUpdateDrawer,
addInstancePermalink: InstancePermalinksAddDrawer,
+ reviewSubmission: SubmissionReviewDrawer,
};
export function useDrawerHelper() {
diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json
index 9d1ac3bb4..9dbd0f425 100644
--- a/packages/admin/locales/en.json
+++ b/packages/admin/locales/en.json
@@ -418,7 +418,8 @@
"entity_purge_modal_header": "Delete {{ entityType }}",
"button_label": "Purge",
"confirm_body": "Are you sure you want to proceed? This action cannot be undone and may remove many entities."
- }
+ },
+ "review_submission": "Review Submission"
},
"sort": {
"ascending": "ascending",
@@ -570,7 +571,13 @@
"field": "Field",
"pattern": "Pattern",
"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."
+ "schema_fields_placeholder": "Schema-specific fields will appear here after this feature is fully connected.",
+ "decision": "Decision",
+ "decision_approve": "Approve",
+ "decision_request_revisions": "Request Revisions",
+ "decision_reject": "Reject",
+ "comments_for_depositor": "Comments for Depositor",
+ "internal_notes": "Internal Notes"
},
"validation": {
"year": "Must be a four digit year",
From 81e7b68cd722ab91fea140ad72eca2df302a9a98 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 24 Feb 2026 14:15:05 -0800
Subject: [PATCH 05/95] [C] Scaffold submission detail + edit form
---
.../SubmissionEditFormMutation.graphql.ts | 250 ++++++++++++++++
.../editMySubmissionQuery.graphql.ts | 126 ++++++++
.../editSubmissionQuery.graphql.ts | 126 ++++++++
.../SubmissionDetails/SubmissionDetails.tsx | 39 +++
.../submission/SubmissionDetails/index.ts | 1 +
.../SubmissionEditForm/SubmissionEditForm.tsx | 272 ++++++++++++++++++
.../submission/SubmissionEditForm/index.ts | 1 +
.../SubmissionLayout.styles.ts | 18 ++
.../SubmissionLayout/SubmissionLayout.tsx | 86 ++++--
packages/admin/locales/en.json | 12 +-
.../pages/my-submissions/[slug]/_layout.tsx | 17 +-
.../my-submissions/[slug]/details/edit.tsx | 42 +++
.../[slug]/details/index.tsx} | 9 +-
.../pages/submissions/[slug]/_layout.tsx | 12 +-
.../pages/submissions/[slug]/details/edit.tsx | 42 +++
.../[slug]/details/index.tsx} | 9 +-
packages/admin/routes/baseRoutes.ts | 14 +
17 files changed, 1023 insertions(+), 53 deletions(-)
create mode 100644 packages/admin/__generated__/SubmissionEditFormMutation.graphql.ts
create mode 100644 packages/admin/__generated__/editMySubmissionQuery.graphql.ts
create mode 100644 packages/admin/__generated__/editSubmissionQuery.graphql.ts
create mode 100644 packages/admin/components/composed/submission/SubmissionDetails/SubmissionDetails.tsx
create mode 100644 packages/admin/components/composed/submission/SubmissionDetails/index.ts
create mode 100644 packages/admin/components/composed/submission/SubmissionEditForm/SubmissionEditForm.tsx
create mode 100644 packages/admin/components/composed/submission/SubmissionEditForm/index.ts
create mode 100644 packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.styles.ts
create mode 100644 packages/admin/pages/my-submissions/[slug]/details/edit.tsx
rename packages/admin/pages/{submissions/[slug]/details.tsx => my-submissions/[slug]/details/index.tsx} (68%)
create mode 100644 packages/admin/pages/submissions/[slug]/details/edit.tsx
rename packages/admin/pages/{my-submissions/[slug]/details.tsx => submissions/[slug]/details/index.tsx} (84%)
diff --git a/packages/admin/__generated__/SubmissionEditFormMutation.graphql.ts b/packages/admin/__generated__/SubmissionEditFormMutation.graphql.ts
new file mode 100644
index 000000000..0bea404e2
--- /dev/null
+++ b/packages/admin/__generated__/SubmissionEditFormMutation.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 SubmissionEditFormMutation$variables = {
+ input: CreateItemInput;
+};
+export type SubmissionEditFormMutation$data = {
+ readonly createItem: {
+ readonly item: {
+ readonly slug: string;
+ readonly title: string;
+ } | null | undefined;
+ readonly " $fragmentSpreads": FragmentRefs<"MutationForm_mutationErrors">;
+ } | null | undefined;
+};
+export type SubmissionEditFormMutation = {
+ response: SubmissionEditFormMutation$data;
+ variables: SubmissionEditFormMutation$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": "SubmissionEditFormMutation",
+ "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": "SubmissionEditFormMutation",
+ "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": "7f1a419931c8d76a472282a6127e1b11",
+ "id": null,
+ "metadata": {},
+ "name": "SubmissionEditFormMutation",
+ "operationKind": "mutation",
+ "text": "mutation SubmissionEditFormMutation(\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 = "5570e12fd7028298d57e592f7cbab182";
+
+export default node;
diff --git a/packages/admin/__generated__/editMySubmissionQuery.graphql.ts b/packages/admin/__generated__/editMySubmissionQuery.graphql.ts
new file mode 100644
index 000000000..d3fdf0263
--- /dev/null
+++ b/packages/admin/__generated__/editMySubmissionQuery.graphql.ts
@@ -0,0 +1,126 @@
+/**
+ * @generated SignedSource<<19c060f379129b376d8599c9615ea813>>
+ * @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 editMySubmissionQuery$variables = {
+ schemaKind: SchemaKind;
+};
+export type editMySubmissionQuery$data = {
+ readonly " $fragmentSpreads": FragmentRefs<"SchemaSelectFragment">;
+};
+export type editMySubmissionQuery = {
+ response: editMySubmissionQuery$data;
+ variables: editMySubmissionQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "schemaKind"
+ }
+];
+return {
+ "fragment": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "editMySubmissionQuery",
+ "selections": [
+ {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "SchemaSelectFragment"
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Operation",
+ "name": "editMySubmissionQuery",
+ "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": "3ce7acdb4e5340996310dc88a529599b",
+ "id": null,
+ "metadata": {},
+ "name": "editMySubmissionQuery",
+ "operationKind": "query",
+ "text": "query editMySubmissionQuery(\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 = "250b6169828bd4dc219c44aae7f44fd3";
+
+export default node;
diff --git a/packages/admin/__generated__/editSubmissionQuery.graphql.ts b/packages/admin/__generated__/editSubmissionQuery.graphql.ts
new file mode 100644
index 000000000..bbc817aa5
--- /dev/null
+++ b/packages/admin/__generated__/editSubmissionQuery.graphql.ts
@@ -0,0 +1,126 @@
+/**
+ * @generated SignedSource<>
+ * @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 editSubmissionQuery$variables = {
+ schemaKind: SchemaKind;
+};
+export type editSubmissionQuery$data = {
+ readonly " $fragmentSpreads": FragmentRefs<"SchemaSelectFragment">;
+};
+export type editSubmissionQuery = {
+ response: editSubmissionQuery$data;
+ variables: editSubmissionQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "schemaKind"
+ }
+];
+return {
+ "fragment": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "editSubmissionQuery",
+ "selections": [
+ {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "SchemaSelectFragment"
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Operation",
+ "name": "editSubmissionQuery",
+ "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": "e31ad615cbd3505703d3bee6e356cea0",
+ "id": null,
+ "metadata": {},
+ "name": "editSubmissionQuery",
+ "operationKind": "query",
+ "text": "query editSubmissionQuery(\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 = "73f595b283a78b9707acffeb35653fee";
+
+export default node;
diff --git a/packages/admin/components/composed/submission/SubmissionDetails/SubmissionDetails.tsx b/packages/admin/components/composed/submission/SubmissionDetails/SubmissionDetails.tsx
new file mode 100644
index 000000000..ca265dbab
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionDetails/SubmissionDetails.tsx
@@ -0,0 +1,39 @@
+import { Forms } from "components/api/MutationForm";
+import { formatDate } from "@wdp/lib/helpers";
+import { useRouteSlug } from "hooks";
+import MockInput from "components/forms/MockInput";
+import {
+ MOCK_SUBMISSIONS,
+ type SubmissionNode,
+} from "components/composed/submission/SubmissionList/mockData";
+
+export default function SubmissionDetails() {
+ const slug = useRouteSlug();
+ const submission: SubmissionNode | undefined = MOCK_SUBMISSIONS.find(
+ (s) => s.slug === slug,
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin/components/composed/submission/SubmissionDetails/index.ts b/packages/admin/components/composed/submission/SubmissionDetails/index.ts
new file mode 100644
index 000000000..1b08ebb6a
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionDetails/index.ts
@@ -0,0 +1 @@
+export { default } from "./SubmissionDetails";
diff --git a/packages/admin/components/composed/submission/SubmissionEditForm/SubmissionEditForm.tsx b/packages/admin/components/composed/submission/SubmissionEditForm/SubmissionEditForm.tsx
new file mode 100644
index 000000000..c18c1fc3e
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionEditForm/SubmissionEditForm.tsx
@@ -0,0 +1,272 @@
+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 { useRouteSlug } from "hooks";
+import useIsMobile from "hooks/useIsMobile";
+import {
+ MOCK_SUBMISSIONS,
+ type SubmissionNode,
+} from "components/composed/submission/SubmissionList/mockData";
+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 SubmissionEditForm({ data }: Props) {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const slug = useRouteSlug();
+
+ const routePrefix = router.pathname.startsWith("/my-submissions")
+ ? "my-submissions"
+ : "submissions";
+ const detailRoute = `${routePrefix}.detail.details`;
+
+ const submission: SubmissionNode | undefined = MOCK_SUBMISSIONS.find(
+ (s) => s.slug === slug,
+ );
+
+ const redirectToDetail = useCallback(() => {
+ const route = RouteHelper.findRouteByName(detailRoute);
+ if (route) {
+ router.push({
+ pathname: route.path,
+ query: { slug },
+ });
+ }
+ }, [router, detailRoute, slug]);
+
+ const onSuccess = useOnSuccess(() => {
+ redirectToDetail();
+ }, [redirectToDetail]);
+
+ const toVariables = useToVariables(
+ (data) => ({
+ input: {
+ title: data.title,
+ 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 = {
+ title: submission?.title ?? "",
+ visibility: "VISIBLE" as Fields["visibility"],
+ 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={redirectToDetail}
+ successNotification="messages.update.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 (
+
+ );
+}
+
+function FilesFieldArray({
+ control,
+ register,
+}: {
+ control: Control;
+ register: UseFormRegister;
+}) {
+ const { t } = useTranslation();
+ const isMobile = useIsMobile();
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: "files",
+ });
+
+ return (
+
+ );
+}
+
+const mutation = graphql`
+ mutation SubmissionEditFormMutation($input: CreateItemInput!) {
+ createItem(input: $input) {
+ item {
+ title
+ slug
+ }
+ ...MutationForm_mutationErrors
+ }
+ }
+`;
diff --git a/packages/admin/components/composed/submission/SubmissionEditForm/index.ts b/packages/admin/components/composed/submission/SubmissionEditForm/index.ts
new file mode 100644
index 000000000..43048e826
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionEditForm/index.ts
@@ -0,0 +1 @@
+export { default } from "./SubmissionEditForm";
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.styles.ts b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.styles.ts
new file mode 100644
index 000000000..0d4a8fcd4
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.styles.ts
@@ -0,0 +1,18 @@
+import styled from "styled-components";
+
+export const NavRow = styled.nav`
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+`;
+
+export const ReviewNextLink = styled.a`
+ display: flex;
+ align-items: center;
+ font-size: var(--font-size-sm);
+ white-space: nowrap;
+
+ > * + * {
+ margin-inline-start: 5px;
+ }
+`;
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
index b72c02c84..5c34a5ba8 100644
--- a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
+++ b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
@@ -1,44 +1,80 @@
import { useTranslation } from "react-i18next";
+import { useRouter } from "next/router";
import { graphql } from "react-relay";
import { useChildRouteLinks, useMaybeFragment, useRouteSlug } from "hooks";
-import { ButtonControlGroup, ButtonControlDrawer } from "components/atomic";
+import {
+ ButtonControlGroup,
+ ButtonControlDrawer,
+ ButtonControlRoute,
+ NamedLink,
+} from "components/atomic";
+import { IconFactory } from "components/factories";
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";
+import * as Styled from "./SubmissionLayout.styles";
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 { t } = useTranslation();
- const routeSlug = useRouteSlug() || undefined;
- const slug = slugProp || routeSlug;
+ const router = useRouter();
+ const slug = useRouteSlug() || undefined;
+
+ // Derive route names from the current pathname
+ const routePrefix = router.pathname.startsWith("/my-submissions")
+ ? "my-submissions"
+ : "submissions";
+ const parentRoute = routePrefix;
+ const detailRoute = `${routePrefix}.detail`;
+ const editRoute = `${routePrefix}.detail.details.edit`;
+
const tabRoutes = useChildRouteLinks(detailRoute, { slug });
const item = useMaybeFragment(fragment, data);
+ const isEditing = router.asPath.endsWith("/edit");
// Fall back to mock data title when no API data is available
const title =
item?.title || MOCK_SUBMISSIONS.find((s) => s.slug === slug)?.title;
- const buttons = parentRoute === "submissions" && (
+ // TODO (pagination): When real API data replaces mocks, "Review Next" will
+ // need to fetch the next page of results at the boundary (e.g. after the last
+ // submission on a page, fetch the next page). For now we use the mock array.
+ const currentIndex = MOCK_SUBMISSIONS.findIndex((s) => s.slug === slug);
+ const nextSlug =
+ currentIndex >= 0 && currentIndex < MOCK_SUBMISSIONS.length - 1
+ ? MOCK_SUBMISSIONS[currentIndex + 1].slug
+ : null;
+
+ const buttons = isEditing ? (
-
- {t("actions.review_submission")}
-
+ {t("common.cancel")}
+
+
+ ) : (
+
+
+ {t("common.edit")}
+
+ {parentRoute === "submissions" && (
+
+ {t("actions.review_submission")}
+
+ )}
);
@@ -46,12 +82,26 @@ export default function SubmissionLayout({
<>
-
+
+
+ {nextSlug && parentRoute === "submissions" && (
+
+
+ {t("actions.review_next")}
+
+
+
+ )}
+
{children}
diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json
index 9dbd0f425..d9e585113 100644
--- a/packages/admin/locales/en.json
+++ b/packages/admin/locales/en.json
@@ -262,7 +262,8 @@
"initial_ordering_success": "Successfully updated initial ordering.",
"initial_ordering_failure": "Failed to update initial ordering.",
"harvest_source_success": "Harvest source updated successfully.",
- "harvest_mapping_success": "Harvest mapping updated successfully."
+ "harvest_mapping_success": "Harvest mapping updated successfully.",
+ "submission_success": "Submission updated successfully."
},
"create": {
"success": "$t({{ name }}) created successfully.",
@@ -419,7 +420,8 @@
"button_label": "Purge",
"confirm_body": "Are you sure you want to proceed? This action cannot be undone and may remove many entities."
},
- "review_submission": "Review Submission"
+ "review_submission": "Review",
+ "review_next": "Review Next"
},
"sort": {
"ascending": "ascending",
@@ -577,7 +579,11 @@
"decision_request_revisions": "Request Revisions",
"decision_reject": "Reject",
"comments_for_depositor": "Comments for Depositor",
- "internal_notes": "Internal Notes"
+ "internal_notes": "Internal Notes",
+ "status": "Status",
+ "last_updated": "Last Updated",
+ "contributors": "Contributors",
+ "files": "Files"
},
"validation": {
"year": "Must be a four digit year",
diff --git a/packages/admin/pages/my-submissions/[slug]/_layout.tsx b/packages/admin/pages/my-submissions/[slug]/_layout.tsx
index 2fcb5d290..e88a3841b 100644
--- a/packages/admin/pages/my-submissions/[slug]/_layout.tsx
+++ b/packages/admin/pages/my-submissions/[slug]/_layout.tsx
@@ -9,19 +9,20 @@ 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 { useRouteSlug } from "hooks";
+// 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 ;
+ const { PageComponent, pageComponentProps } = props;
// TODO: Uncomment when real submission API is available
+ // const slug = useRouteSlug() as string;
+ // if (!slug) return ;
+ //
// const queryVars = useBaseListQueryVars();
// const searchParams = useSearchParams();
//
@@ -36,17 +37,15 @@ export default function Layout(props: Props) {
// 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={{
diff --git a/packages/admin/pages/my-submissions/[slug]/details/edit.tsx b/packages/admin/pages/my-submissions/[slug]/details/edit.tsx
new file mode 100644
index 000000000..bc39fd94f
--- /dev/null
+++ b/packages/admin/pages/my-submissions/[slug]/details/edit.tsx
@@ -0,0 +1,42 @@
+import { usePreloadedQuery, graphql, PreloadedQuery } from "react-relay";
+import { QueryTransitionWrapper } from "@wdp/lib/api/components";
+import { LoadingPage } from "components/atomic/loading";
+import SubmissionEditForm from "components/composed/submission/SubmissionEditForm";
+import type { editMySubmissionQuery as Query } from "@/relay/editMySubmissionQuery.graphql";
+import Layout from "../_layout";
+import type { GetLayout } from "@wdp/lib/types/page";
+
+function EditSubmission() {
+ return (
+
+ query={query}
+ variables={{ schemaKind: "ITEM" }}
+ loadingFallback={}
+ >
+ {({ queryRef }) =>
+ queryRef ? : null
+ }
+
+ );
+}
+
+const FormWithData = ({ queryRef }: { queryRef: PreloadedQuery }) => {
+ const data = usePreloadedQuery(query, queryRef);
+
+ return ;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const getLayout: GetLayout = (props) => {
+ return ;
+};
+
+EditSubmission.getLayout = getLayout;
+
+export default EditSubmission;
+
+const query = graphql`
+ query editMySubmissionQuery($schemaKind: SchemaKind!) {
+ ...SchemaSelectFragment
+ }
+`;
diff --git a/packages/admin/pages/submissions/[slug]/details.tsx b/packages/admin/pages/my-submissions/[slug]/details/index.tsx
similarity index 68%
rename from packages/admin/pages/submissions/[slug]/details.tsx
rename to packages/admin/pages/my-submissions/[slug]/details/index.tsx
index 8a91f930b..022859f0e 100644
--- a/packages/admin/pages/submissions/[slug]/details.tsx
+++ b/packages/admin/pages/my-submissions/[slug]/details/index.tsx
@@ -1,12 +1,9 @@
-import Layout from "./_layout";
+import SubmissionDetailsView from "components/composed/submission/SubmissionDetails";
+import Layout from "../_layout";
import type { GetLayout } from "@wdp/lib/types/page";
function SubmissionDetails() {
- return (
-
- Submission details coming soon.
-
- );
+ return ;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/packages/admin/pages/submissions/[slug]/_layout.tsx b/packages/admin/pages/submissions/[slug]/_layout.tsx
index ae8786906..dd00df879 100644
--- a/packages/admin/pages/submissions/[slug]/_layout.tsx
+++ b/packages/admin/pages/submissions/[slug]/_layout.tsx
@@ -2,23 +2,13 @@ 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 (
-
+
);
diff --git a/packages/admin/pages/submissions/[slug]/details/edit.tsx b/packages/admin/pages/submissions/[slug]/details/edit.tsx
new file mode 100644
index 000000000..044dce037
--- /dev/null
+++ b/packages/admin/pages/submissions/[slug]/details/edit.tsx
@@ -0,0 +1,42 @@
+import { usePreloadedQuery, graphql, PreloadedQuery } from "react-relay";
+import { QueryTransitionWrapper } from "@wdp/lib/api/components";
+import { LoadingPage } from "components/atomic/loading";
+import SubmissionEditForm from "components/composed/submission/SubmissionEditForm";
+import type { editSubmissionQuery as Query } from "@/relay/editSubmissionQuery.graphql";
+import Layout from "../_layout";
+import type { GetLayout } from "@wdp/lib/types/page";
+
+function EditSubmission() {
+ return (
+
+ query={query}
+ variables={{ schemaKind: "ITEM" }}
+ loadingFallback={}
+ >
+ {({ queryRef }) =>
+ queryRef ? : null
+ }
+
+ );
+}
+
+const FormWithData = ({ queryRef }: { queryRef: PreloadedQuery }) => {
+ const data = usePreloadedQuery(query, queryRef);
+
+ return ;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const getLayout: GetLayout = (props) => {
+ return ;
+};
+
+EditSubmission.getLayout = getLayout;
+
+export default EditSubmission;
+
+const query = graphql`
+ query editSubmissionQuery($schemaKind: SchemaKind!) {
+ ...SchemaSelectFragment
+ }
+`;
diff --git a/packages/admin/pages/my-submissions/[slug]/details.tsx b/packages/admin/pages/submissions/[slug]/details/index.tsx
similarity index 84%
rename from packages/admin/pages/my-submissions/[slug]/details.tsx
rename to packages/admin/pages/submissions/[slug]/details/index.tsx
index ee0bb8f2a..14a1da2d6 100644
--- a/packages/admin/pages/my-submissions/[slug]/details.tsx
+++ b/packages/admin/pages/submissions/[slug]/details/index.tsx
@@ -1,17 +1,14 @@
// import { graphql, usePreloadedQuery, PreloadedQuery } from "react-relay";
// import type { detailsSubmissionQuery as Query } from "@/relay/detailsSubmissionQuery.graphql";
-import Layout from "./_layout";
+import SubmissionDetailsView from "components/composed/submission/SubmissionDetails";
+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.
-
- );
+ return ;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/packages/admin/routes/baseRoutes.ts b/packages/admin/routes/baseRoutes.ts
index 652054766..c478051c5 100644
--- a/packages/admin/routes/baseRoutes.ts
+++ b/packages/admin/routes/baseRoutes.ts
@@ -273,6 +273,13 @@ export const baseRoutes: BaseRoute[] = [
name: "my-submissions.detail.details",
path: "/my-submissions/[slug]/details",
label: "nav.details",
+ routes: [
+ {
+ name: "my-submissions.detail.details.edit",
+ path: "/my-submissions/[slug]/details/edit",
+ label: "common.edit",
+ },
+ ],
},
{
name: "my-submissions.detail.history",
@@ -298,6 +305,13 @@ export const baseRoutes: BaseRoute[] = [
name: "submissions.detail.details",
path: "/submissions/[slug]/details",
label: "nav.details",
+ routes: [
+ {
+ name: "submissions.detail.details.edit",
+ path: "/submissions/[slug]/details/edit",
+ label: "common.edit",
+ },
+ ],
},
{
name: "submissions.detail.history",
From dbf720ef9ce1fa65e0f9631f22d11980386ad3fe Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 24 Feb 2026 16:36:58 -0800
Subject: [PATCH 06/95] [C] Update CI to match node/yarn versions
---
.github/workflows/continuous-integration.yml | 34 ++++----------------
package.json | 5 ++-
2 files changed, 9 insertions(+), 30 deletions(-)
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 104a45cb5..171b81af7 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -14,34 +14,14 @@ jobs:
name: Lint Javascript
steps:
- - name: "Check out code"
- uses: actions/checkout@v4
-
- - name: "Install Node"
- uses: actions/setup-node@v4
- with:
- node-version: "24.x"
-
- - name: "Restore Node dependencies from cache"
- uses: actions/cache@v4
+ - uses: actions/checkout@v4
+ - run: corepack enable
+ - uses: actions/setup-node@v4
with:
- path: node_modules
- key: ${{ runner.OS }}-node-module-cache-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.OS }}-node-module-cache-
-
- - name: "Install Node dependencies"
- env:
- NODE_ENV: development
- run: |
- corepack enable
- yarn install
- working-directory: ./
-
- - name: "Lint Client"
- run: |
- yarn lint
- working-directory: ./
+ node-version-file: "package.json"
+ cache: "yarn"
+ - run: yarn install
+ - run: yarn lint
build:
runs-on: ubuntu-latest
diff --git a/package.json b/package.json
index 316f2c67d..ba0961824 100644
--- a/package.json
+++ b/package.json
@@ -18,14 +18,13 @@
"description": "NGLP Web Delivery Platform client application",
"main": "index.js",
"engines": {
- "node": "20.x"
+ "node": "24.x"
},
"packageManager": "yarn@4.13.0",
"scripts": {
"dev": "yarn workspace @wdp/admin run dev",
"build": "scripts/do.sh build",
- "build:ci": "yarn build:admin",
- "build:admin": "yarn workspace @wdp/admin run build",
+ "build:ci": "yarn workspace @wdp/admin run build",
"start": "scripts/do.sh start",
"start:admin": "yarn workspace @wdp/admin run next start -p ${PORT:-3000}",
"lint": "concurrently -c \"auto\" \"yarn:lint:*\"",
From 62baf24f4ce54702dbb28e85737a60c8bb6601a5 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 24 Feb 2026 18:39:39 -0800
Subject: [PATCH 07/95] [F] Add StatusBadge + ReviewComment atomics
---
.../ReviewFeedback/ReviewFeedback.styles.ts | 17 +++++++++++
.../atomic/ReviewFeedback/ReviewFeedback.tsx | 17 +++++++++++
.../components/atomic/ReviewFeedback/index.ts | 1 +
.../atomic/StatusBadge/StatusBadge.styles.ts | 14 +++++++++
.../atomic/StatusBadge/StatusBadge.tsx | 30 +++++++++++++++++++
.../components/atomic/StatusBadge/index.ts | 1 +
packages/admin/components/atomic/index.ts | 2 ++
.../submission/SubmissionList/mockData.ts | 13 ++++----
packages/admin/theme/base/variables.ts | 1 +
9 files changed, 91 insertions(+), 5 deletions(-)
create mode 100644 packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.styles.ts
create mode 100644 packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.tsx
create mode 100644 packages/admin/components/atomic/ReviewFeedback/index.ts
create mode 100644 packages/admin/components/atomic/StatusBadge/StatusBadge.styles.ts
create mode 100644 packages/admin/components/atomic/StatusBadge/StatusBadge.tsx
create mode 100644 packages/admin/components/atomic/StatusBadge/index.ts
diff --git a/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.styles.ts b/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.styles.ts
new file mode 100644
index 000000000..2afdf22a5
--- /dev/null
+++ b/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.styles.ts
@@ -0,0 +1,17 @@
+import styled from "styled-components";
+import { pxToRem } from "theme/mixins/functions";
+
+export const Wrapper = styled.div`
+ border-radius: ${pxToRem(10)};
+ padding: ${pxToRem(20)} ${pxToRem(24)};
+ background-color: var(--yellowtint);
+ border: 1px solid var(--yellowdark);
+ margin-block-end: var(--form-grid-row-gap);
+`;
+
+export const Header = styled.h3`
+ padding-block-end: ${pxToRem(12)};
+ color: var(--yellowdark);
+`;
+
+export const Message = styled.div``;
diff --git a/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.tsx b/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.tsx
new file mode 100644
index 000000000..30b7bb2fb
--- /dev/null
+++ b/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.tsx
@@ -0,0 +1,17 @@
+import * as Styled from "./ReviewFeedback.styles";
+
+const ReviewFeedback = ({ name, message }: Props) => {
+ return (
+
+ {name}
+ {message && {message}}
+
+ );
+};
+
+interface Props {
+ name: string;
+ message?: string | React.JSX.Element;
+}
+
+export default ReviewFeedback;
diff --git a/packages/admin/components/atomic/ReviewFeedback/index.ts b/packages/admin/components/atomic/ReviewFeedback/index.ts
new file mode 100644
index 000000000..bc2d3fd7c
--- /dev/null
+++ b/packages/admin/components/atomic/ReviewFeedback/index.ts
@@ -0,0 +1 @@
+export { default as ReviewFeedback } from "./ReviewFeedback";
diff --git a/packages/admin/components/atomic/StatusBadge/StatusBadge.styles.ts b/packages/admin/components/atomic/StatusBadge/StatusBadge.styles.ts
new file mode 100644
index 000000000..17254b415
--- /dev/null
+++ b/packages/admin/components/atomic/StatusBadge/StatusBadge.styles.ts
@@ -0,0 +1,14 @@
+import styled from "styled-components";
+
+export const Badge = styled.span<{ $bg: string; $color: string }>`
+ display: inline-block;
+ padding: 4px 12px;
+ border-radius: var(--border-radius-xlg);
+ font-size: var(--font-size-label-sm);
+ font-weight: var(--font-weight-medium);
+ white-space: nowrap;
+ text-align: center;
+ background-color: ${({ $bg }) => $bg};
+ color: ${({ $color }) => $color};
+ align-self: baseline;
+`;
diff --git a/packages/admin/components/atomic/StatusBadge/StatusBadge.tsx b/packages/admin/components/atomic/StatusBadge/StatusBadge.tsx
new file mode 100644
index 000000000..76b19b384
--- /dev/null
+++ b/packages/admin/components/atomic/StatusBadge/StatusBadge.tsx
@@ -0,0 +1,30 @@
+import * as Styled from "./StatusBadge.styles";
+
+const STATUS_COLORS: Record = {
+ Draft: { bg: "var(--neutral10)", color: "var(--neutral80)" },
+ "In Review": { bg: "var(--brand20)", color: "var(--brand100)" },
+ "Revisions Requested": {
+ bg: "var(--yellowtint)",
+ color: "var(--yellowdark)",
+ },
+ Published: { bg: "var(--greentint)", color: "var(--greendark)" },
+};
+
+const FALLBACK_COLORS = { bg: "var(--neutral10)", color: "var(--neutral80)" };
+
+interface Props {
+ status: string;
+ className?: string;
+}
+
+const StatusBadge = ({ status, className }: Props) => {
+ const colors = STATUS_COLORS[status] ?? FALLBACK_COLORS;
+
+ return (
+
+ {status}
+
+ );
+};
+
+export default StatusBadge;
diff --git a/packages/admin/components/atomic/StatusBadge/index.ts b/packages/admin/components/atomic/StatusBadge/index.ts
new file mode 100644
index 000000000..f12e5e1d6
--- /dev/null
+++ b/packages/admin/components/atomic/StatusBadge/index.ts
@@ -0,0 +1 @@
+export { default as StatusBadge } from "./StatusBadge";
diff --git a/packages/admin/components/atomic/index.ts b/packages/admin/components/atomic/index.ts
index 25fd4527f..c87bd7626 100644
--- a/packages/admin/components/atomic/index.ts
+++ b/packages/admin/components/atomic/index.ts
@@ -13,4 +13,6 @@ export * from "./buttons";
export * from "./links";
export * from "./loading";
export * from "./MessageBlock";
+export * from "./ReviewFeedback";
+export * from "./StatusBadge";
export * from "./images";
diff --git a/packages/admin/components/composed/submission/SubmissionList/mockData.ts b/packages/admin/components/composed/submission/SubmissionList/mockData.ts
index a5fcde577..693afaa8a 100644
--- a/packages/admin/components/composed/submission/SubmissionList/mockData.ts
+++ b/packages/admin/components/composed/submission/SubmissionList/mockData.ts
@@ -7,6 +7,7 @@ export interface SubmissionNode {
collection: string;
updatedAt: string;
allowedActions: string[];
+ reviewComment?: string;
}
export const MOCK_SUBMISSIONS: readonly SubmissionNode[] = [
@@ -17,16 +18,18 @@ export const MOCK_SUBMISSIONS: readonly SubmissionNode[] = [
status: "Draft",
collection: "Digital Humanities Quarterly",
updatedAt: "2026-02-20T14:30:00Z",
- allowedActions: [],
+ allowedActions: ["self.update"],
},
{
id: "mock-submission-2",
slug: "mock-submission-2",
title: "Open Access and Community Engagement",
- status: "In Review",
+ status: "Revisions Requested",
collection: "Library Science Research",
updatedAt: "2026-02-18T09:15:00Z",
- allowedActions: [],
+ allowedActions: ["self.update"],
+ reviewComment:
+ "The methodology section needs more detail on the data collection process. Please also add citations for the claims made in the introduction.",
},
{
id: "mock-submission-3",
@@ -44,7 +47,7 @@ export const MOCK_SUBMISSIONS: readonly SubmissionNode[] = [
status: "Draft",
collection: "Digital Humanities Quarterly",
updatedAt: "2026-02-22T11:00:00Z",
- allowedActions: [],
+ allowedActions: ["self.update"],
},
{
id: "mock-submission-5",
@@ -53,6 +56,6 @@ export const MOCK_SUBMISSIONS: readonly SubmissionNode[] = [
status: "In Review",
collection: "Archives & Records",
updatedAt: "2026-02-10T08:30:00Z",
- allowedActions: [],
+ allowedActions: ["self.update"],
},
];
diff --git a/packages/admin/theme/base/variables.ts b/packages/admin/theme/base/variables.ts
index eb90ceb92..17f24e9dc 100644
--- a/packages/admin/theme/base/variables.ts
+++ b/packages/admin/theme/base/variables.ts
@@ -43,6 +43,7 @@ export const colors = {
},
yellow: {
dark: "#fab048",
+ tint: "#FFF3E0",
},
};
From 0f2903865ef1ba5accb018c3e6f18b66c105d645 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 24 Feb 2026 18:40:37 -0800
Subject: [PATCH 08/95] [F] Add StatusColumn
---
.../StatusColumn/StatusColumn.tsx | 32 +++++++++++++++++++
.../model/ModelColumns/StatusColumn/index.ts | 1 +
.../composed/model/ModelColumns/index.tsx | 2 ++
3 files changed, 35 insertions(+)
create mode 100644 packages/admin/components/composed/model/ModelColumns/StatusColumn/StatusColumn.tsx
create mode 100644 packages/admin/components/composed/model/ModelColumns/StatusColumn/index.ts
diff --git a/packages/admin/components/composed/model/ModelColumns/StatusColumn/StatusColumn.tsx b/packages/admin/components/composed/model/ModelColumns/StatusColumn/StatusColumn.tsx
new file mode 100644
index 000000000..30e2bcffd
--- /dev/null
+++ b/packages/admin/components/composed/model/ModelColumns/StatusColumn/StatusColumn.tsx
@@ -0,0 +1,32 @@
+import get from "lodash/get";
+import styled from "styled-components";
+import { StatusBadge } from "components/atomic";
+import { PartialColumnish, Node } from "../types";
+import type { ColumnDef } from "@tanstack/react-table";
+
+const ListBadge = styled(StatusBadge)`
+ min-width: 160px;
+`;
+
+type Props = PartialColumnish & {
+ header: () => string | React.ReactNode;
+};
+
+const StatusColumn = ({
+ header,
+ ...props
+}: Props): ColumnDef => {
+ return {
+ header,
+ id: "status",
+ accessorFn: (originalRow: T) => get(originalRow, "status"),
+ enableSorting: false,
+ cell: (info) => {
+ const value = info.getValue();
+ return ;
+ },
+ ...props,
+ };
+};
+
+export default StatusColumn;
diff --git a/packages/admin/components/composed/model/ModelColumns/StatusColumn/index.ts b/packages/admin/components/composed/model/ModelColumns/StatusColumn/index.ts
new file mode 100644
index 000000000..b0184a5f9
--- /dev/null
+++ b/packages/admin/components/composed/model/ModelColumns/StatusColumn/index.ts
@@ -0,0 +1 @@
+export { default } from "./StatusColumn";
diff --git a/packages/admin/components/composed/model/ModelColumns/index.tsx b/packages/admin/components/composed/model/ModelColumns/index.tsx
index e8dc4f6ba..d9ef5f21a 100644
--- a/packages/admin/components/composed/model/ModelColumns/index.tsx
+++ b/packages/admin/components/composed/model/ModelColumns/index.tsx
@@ -20,6 +20,7 @@ import PositionColumn from "./PositionColumn";
import BeganAtColumn from "./BeganAtColumn";
import EndedAtColumn from "./EndedAtColumn";
import LinkColumn from "./LinkColumn";
+import StatusColumn from "./StatusColumn";
const modelColumns = {
CreatedAtColumn,
@@ -44,6 +45,7 @@ const modelColumns = {
BeganAtColumn,
EndedAtColumn,
LinkColumn,
+ StatusColumn,
};
export default modelColumns;
From 309a7706d20b5ef92d44a5208f7585fb6b07ce85 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 24 Feb 2026 18:41:27 -0800
Subject: [PATCH 09/95] [F] Add submit + condition actions on status
---
.../SubmissionLayout/SubmissionLayout.tsx | 60 ++++++++++++++++---
.../SubmissionList/SubmissionList.tsx | 26 +++++---
.../layout/ContentHeader/ContentHeader.tsx | 4 ++
.../layout/PageHeader/PageHeader.tsx | 4 ++
packages/admin/locales/en.json | 9 ++-
5 files changed, 88 insertions(+), 15 deletions(-)
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
index 5c34a5ba8..aa16ca94a 100644
--- a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
+++ b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
@@ -4,9 +4,12 @@ import { graphql } from "react-relay";
import { useChildRouteLinks, useMaybeFragment, useRouteSlug } from "hooks";
import {
ButtonControlGroup,
+ ButtonControlConfirm,
ButtonControlDrawer,
ButtonControlRoute,
NamedLink,
+ ReviewFeedback,
+ StatusBadge,
} from "components/atomic";
import { IconFactory } from "components/factories";
import { PageHeader, BackToAll } from "components/layout";
@@ -38,9 +41,9 @@ export default function SubmissionLayout({
const item = useMaybeFragment(fragment, data);
const isEditing = router.asPath.endsWith("/edit");
- // Fall back to mock data title when no API data is available
- const title =
- item?.title || MOCK_SUBMISSIONS.find((s) => s.slug === slug)?.title;
+ // Fall back to mock data when no API data is available
+ const submission = MOCK_SUBMISSIONS.find((s) => s.slug === slug);
+ const title = item?.title || submission?.title;
// TODO (pagination): When real API data replaces mocks, "Review Next" will
// need to fetch the next page of results at the boundary (e.g. after the last
@@ -51,6 +54,13 @@ export default function SubmissionLayout({
? MOCK_SUBMISSIONS[currentIndex + 1].slug
: null;
+ const isPublished = submission?.status === "Published";
+
+ const handleSubmitForReview = (hideDialog: () => void) => {
+ // TODO: Wire up to real mutation when API is ready
+ hideDialog();
+ };
+
const buttons = isEditing ? (
- ) : (
+ ) : !isPublished ? (
{t("common.edit")}
- {parentRoute === "submissions" && (
+ {parentRoute === "submissions" ? (
{t("actions.review_submission")}
- )}
+ ) : submission?.status === "Draft" ||
+ submission?.status === "Revisions Requested" ? (
+
+ {submission.status === "Draft"
+ ? t("actions.submit_for_review")
+ : t("actions.resubmit_for_review")}
+
+ ) : null}
- );
+ ) : undefined;
return (
<>
@@ -99,10 +134,21 @@ export default function SubmissionLayout({
+ ) : undefined
+ }
tabRoutes={tabRoutes}
tabLinksOnly
buttons={buttons}
/>
+ {submission?.reviewComment && (
+
+ )}
{children}
>
diff --git a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx
index b468942a4..5f091c1f5 100644
--- a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx
+++ b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx
@@ -21,25 +21,30 @@ function SubmissionList({ data }: Props) {
const { t } = useTranslation();
const router = useRouter();
+ const isReview = router.pathname.startsWith("/submissions");
+ const detailRoute = isReview ? "submissions.detail" : "my-submissions.detail";
+
/* Replace real nodes with mock data while preserving pagination fragment refs */
+ const filteredSubmissions = isReview
+ ? MOCK_SUBMISSIONS.filter(
+ (s) => s.status === "In Review" || s.status === "Revisions Requested",
+ )
+ : MOCK_SUBMISSIONS;
+
const dataWithMockNodes = connection
? ({
...connection,
- nodes: MOCK_SUBMISSIONS,
+ nodes: filteredSubmissions,
} 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({
- id: "status",
+ ModelColumns.StatusColumn({
header: () => t("lists.status_column"),
}),
ModelColumns.StringColumn({
@@ -52,9 +57,16 @@ function SubmissionList({ data }: Props) {
];
const actions = {
+ handleEdit: ({ row }: ModelTableActionProps) => {
+ if (row.original.slug) {
+ router.push(
+ `/${isReview ? "submissions" : "my-submissions"}/${row.original.slug}/details/edit`,
+ );
+ }
+ },
handleView: ({ row }: ModelTableActionProps) =>
row.original.slug
- ? `/${isReview ? "submissions" : "my-submissions"}/${row.original.slug}`
+ ? `/${isReview ? "submissions" : "my-submissions"}/${row.original.slug}/details`
: null,
};
diff --git a/packages/admin/components/layout/ContentHeader/ContentHeader.tsx b/packages/admin/components/layout/ContentHeader/ContentHeader.tsx
index 933be8e71..f20699a4c 100644
--- a/packages/admin/components/layout/ContentHeader/ContentHeader.tsx
+++ b/packages/admin/components/layout/ContentHeader/ContentHeader.tsx
@@ -10,6 +10,7 @@ import * as Styled from "./ContentHeader.styles";
const ContentHeader = ({
data,
title,
+ titleTag,
rightSide,
headerStyle = "primary",
}: Props) => {
@@ -24,6 +25,7 @@ const ContentHeader = ({
{title}
+ {titleTag}
{markedForPurge && (
{t("messages.marked_for_purge")}
)}
@@ -41,6 +43,8 @@ const ContentHeader = ({
interface Props {
/** Content title */
title?: string | null;
+ /** Optional tag rendered inline with the title (e.g. status badge) */
+ titleTag?: React.ReactNode;
/** Header style, default "primary" (h1) */
headerStyle?: "primary" | "secondary";
/** Optional right side content node. Can be a single button, multiple buttons, dropdown, etc. */
diff --git a/packages/admin/components/layout/PageHeader/PageHeader.tsx b/packages/admin/components/layout/PageHeader/PageHeader.tsx
index f9dc33cbb..1c4d75db5 100644
--- a/packages/admin/components/layout/PageHeader/PageHeader.tsx
+++ b/packages/admin/components/layout/PageHeader/PageHeader.tsx
@@ -20,6 +20,7 @@ type SidebarNavProps = React.ComponentProps;
*/
const PageHeader = ({
title,
+ titleTag,
breadcrumbsProps,
tabRoutes,
tabLinksOnly,
@@ -39,6 +40,7 @@ const PageHeader = ({
data={data}
headerStyle={headerStyle}
title={title}
+ titleTag={titleTag}
rightSide={buttons}
/>
{tabRoutes && (
@@ -62,6 +64,8 @@ interface Link extends NamedLinkProps {
interface Props extends Pick {
/** Page title */
title?: string | null;
+ /** Optional tag rendered inline with the title (e.g. status badge) */
+ titleTag?: React.ReactNode;
/** Breadcrumb props to be passed down to Breadcrumbs component */
breadcrumbsProps?: BreadcrumbProps;
/** Child tabs (TabNav) */
diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json
index d9e585113..e67ab32dd 100644
--- a/packages/admin/locales/en.json
+++ b/packages/admin/locales/en.json
@@ -421,7 +421,11 @@
"confirm_body": "Are you sure you want to proceed? This action cannot be undone and may remove many entities."
},
"review_submission": "Review",
- "review_next": "Review Next"
+ "review_next": "Review Next",
+ "submit_for_review": "Submit",
+ "submit_for_review_confirm": "Are you sure you're ready to submit this for review? It will be sent to a reviewer for evaluation.",
+ "resubmit_for_review": "Resubmit",
+ "resubmit_for_review_confirm": "Are you sure you're ready to resubmit this for review? It will be sent back to a reviewer for evaluation."
},
"sort": {
"ascending": "ascending",
@@ -698,6 +702,9 @@
"show_all_schemas": "Show all schemas",
"show_less": "Show less"
},
+ "submission_detail": {
+ "revisions_requested": "Revisions Requested"
+ },
"harvesting": {
"mode": "Mode",
"mode_manual": "Manual",
From 6f7f43ab84284ad5425b5ed020060daf3cccc5e9 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 3 Mar 2026 09:45:23 -0800
Subject: [PATCH 10/95] [F] Scaffold collection manage submissions page
---
...ManageSlugCollectionsPagesQuery.graphql.ts | 110 ++++++++++++++++++
.../CollectionSubmissionSettings.tsx | 34 ++++++
.../CollectionSubmissionSettings/index.tsx | 1 +
.../collections/[slug]/manage/submissions.tsx | 31 +++++
packages/admin/routes/baseRoutes.ts | 6 +
5 files changed, 182 insertions(+)
create mode 100644 packages/admin/__generated__/submissionsManageSlugCollectionsPagesQuery.graphql.ts
create mode 100644 packages/admin/components/composed/collection/CollectionSubmissionSettings/CollectionSubmissionSettings.tsx
create mode 100644 packages/admin/components/composed/collection/CollectionSubmissionSettings/index.tsx
create mode 100644 packages/admin/pages/collections/[slug]/manage/submissions.tsx
diff --git a/packages/admin/__generated__/submissionsManageSlugCollectionsPagesQuery.graphql.ts b/packages/admin/__generated__/submissionsManageSlugCollectionsPagesQuery.graphql.ts
new file mode 100644
index 000000000..a2055211b
--- /dev/null
+++ b/packages/admin/__generated__/submissionsManageSlugCollectionsPagesQuery.graphql.ts
@@ -0,0 +1,110 @@
+/**
+ * @generated SignedSource<<49ef2d37b280936ed6cd132aedf21b42>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+export type submissionsManageSlugCollectionsPagesQuery$variables = {
+ slug: string;
+};
+export type submissionsManageSlugCollectionsPagesQuery$data = {
+ readonly collection: {
+ readonly slug: string;
+ } | null | undefined;
+};
+export type submissionsManageSlugCollectionsPagesQuery = {
+ response: submissionsManageSlugCollectionsPagesQuery$data;
+ variables: submissionsManageSlugCollectionsPagesQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "slug"
+ }
+],
+v1 = [
+ {
+ "kind": "Variable",
+ "name": "slug",
+ "variableName": "slug"
+ }
+],
+v2 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "slug",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "submissionsManageSlugCollectionsPagesQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v1/*: any*/),
+ "concreteType": "Collection",
+ "kind": "LinkedField",
+ "name": "collection",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/)
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Operation",
+ "name": "submissionsManageSlugCollectionsPagesQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v1/*: any*/),
+ "concreteType": "Collection",
+ "kind": "LinkedField",
+ "name": "collection",
+ "plural": false,
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "e16f0eb96650aaa2689d040527d55019",
+ "id": null,
+ "metadata": {},
+ "name": "submissionsManageSlugCollectionsPagesQuery",
+ "operationKind": "query",
+ "text": "query submissionsManageSlugCollectionsPagesQuery(\n $slug: Slug!\n) {\n collection(slug: $slug) {\n slug\n id\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "156b3fc2026f046ece1b797997864e50";
+
+export default node;
diff --git a/packages/admin/components/composed/collection/CollectionSubmissionSettings/CollectionSubmissionSettings.tsx b/packages/admin/components/composed/collection/CollectionSubmissionSettings/CollectionSubmissionSettings.tsx
new file mode 100644
index 000000000..67e317329
--- /dev/null
+++ b/packages/admin/components/composed/collection/CollectionSubmissionSettings/CollectionSubmissionSettings.tsx
@@ -0,0 +1,34 @@
+import { useForm } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import capitalize from "lodash/capitalize";
+import { ContentHeader } from "components/layout";
+import Checkbox from "components/forms/Checkbox";
+import FormGrid from "components/forms/FormGrid";
+
+type Fields = {
+ acceptsSubmissions: boolean;
+};
+
+export default function CollectionSubmissionSettings() {
+ const { t } = useTranslation();
+ const { register } = useForm({
+ defaultValues: { acceptsSubmissions: false },
+ });
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/packages/admin/components/composed/collection/CollectionSubmissionSettings/index.tsx b/packages/admin/components/composed/collection/CollectionSubmissionSettings/index.tsx
new file mode 100644
index 000000000..d07dc035a
--- /dev/null
+++ b/packages/admin/components/composed/collection/CollectionSubmissionSettings/index.tsx
@@ -0,0 +1 @@
+export { default } from "./CollectionSubmissionSettings";
diff --git a/packages/admin/pages/collections/[slug]/manage/submissions.tsx b/packages/admin/pages/collections/[slug]/manage/submissions.tsx
new file mode 100644
index 000000000..b06d250a1
--- /dev/null
+++ b/packages/admin/pages/collections/[slug]/manage/submissions.tsx
@@ -0,0 +1,31 @@
+import { graphql, usePreloadedQuery, PreloadedQuery } from "react-relay";
+import CollectionSubmissionSettings from "components/composed/collection/CollectionSubmissionSettings";
+import type { submissionsManageSlugCollectionsPagesQuery as Query } from "@/relay/submissionsManageSlugCollectionsPagesQuery.graphql";
+import Layout from "./_layout";
+import type { GetLayout } from "@wdp/lib/types/page";
+
+function CollectionSubmissions({ queryRef }: Props) {
+ const { collection } = usePreloadedQuery(query, queryRef);
+
+ return collection ? : null;
+}
+
+const getLayout: GetLayout = (props) => (
+
+);
+
+CollectionSubmissions.getLayout = getLayout;
+
+type Props = {
+ queryRef: PreloadedQuery;
+};
+
+const query = graphql`
+ query submissionsManageSlugCollectionsPagesQuery($slug: Slug!) {
+ collection(slug: $slug) {
+ slug
+ }
+ }
+`;
+
+export default CollectionSubmissions;
diff --git a/packages/admin/routes/baseRoutes.ts b/packages/admin/routes/baseRoutes.ts
index c478051c5..b00e075c8 100644
--- a/packages/admin/routes/baseRoutes.ts
+++ b/packages/admin/routes/baseRoutes.ts
@@ -98,6 +98,12 @@ export const baseRoutes: BaseRoute[] = [
label: "nav.permalinks",
actions: ["self.update"],
},
+ {
+ name: "collection.manage.submissions",
+ path: "/collections/[slug]/manage/submissions",
+ label: "glossary.submission_other",
+ actions: ["self.update"],
+ },
],
},
],
From 2a15a0c9566a439c83860d107222bb15858acffe Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 3 Mar 2026 12:43:37 -0800
Subject: [PATCH 11/95] [F] Add publish route + initial UI for bulk actions
---
.../PublishListFragment.graphql.ts | 69 ++++++
.../__generated__/publishQuery.graphql.ts | 216 ++++++++++++++++++
...uery.graphql.ts => reviewQuery.graphql.ts} | 24 +-
.../atomic/StatusBadge/StatusBadge.tsx | 1 +
.../patterns/ButtonControlConfirm.tsx | 2 +
.../composed/model/ModelList/ModelList.tsx | 6 +-
.../model/ModelList/hooks/useModelList.tsx | 22 +-
.../model/ModelList/hooks/useRowActions.tsx | 30 ++-
.../model/ModelListPage/ModelListPage.tsx | 25 +-
.../ModelPageCountActions.tsx | 3 +
.../submission/PublishList/PublishList.tsx | 152 ++++++++++++
.../composed/submission/PublishList/index.tsx | 1 +
.../SubmissionList/SubmissionList.tsx | 44 ++--
.../submission/SubmissionList/mockData.ts | 18 ++
.../PageCountActions/PageCountActions.tsx | 48 ++--
.../components/layout/Table/Table.styles.ts | 28 +--
.../components/layout/Table/TableHeader.tsx | 5 +-
.../bulkActions/SelectAll/SelectAll.styles.ts | 70 ++++++
.../Table/bulkActions/SelectAll/SelectAll.tsx | 96 ++++++++
.../Table/bulkActions/SelectAll/index.ts | 1 +
.../layout/Table/bulkActions/hooks.ts | 133 +++++++++++
.../layout/Table/bulkActions/index.ts | 10 +
.../layout/Table/bulkActions/reducer.ts | 34 +++
.../bulkActions/useBulkSelectionBridge.ts | 58 +++++
packages/admin/locales/en.json | 29 ++-
packages/admin/pages/submissions/index.tsx | 46 +---
packages/admin/pages/submissions/publish.tsx | 44 ++++
packages/admin/pages/submissions/review.tsx | 48 ++++
packages/admin/routes/baseRoutes.ts | 11 +
29 files changed, 1152 insertions(+), 122 deletions(-)
create mode 100644 packages/admin/__generated__/PublishListFragment.graphql.ts
create mode 100644 packages/admin/__generated__/publishQuery.graphql.ts
rename packages/admin/__generated__/{submissionsQuery.graphql.ts => reviewQuery.graphql.ts} (81%)
create mode 100644 packages/admin/components/composed/submission/PublishList/PublishList.tsx
create mode 100644 packages/admin/components/composed/submission/PublishList/index.tsx
create mode 100644 packages/admin/components/layout/Table/bulkActions/SelectAll/SelectAll.styles.ts
create mode 100644 packages/admin/components/layout/Table/bulkActions/SelectAll/SelectAll.tsx
create mode 100644 packages/admin/components/layout/Table/bulkActions/SelectAll/index.ts
create mode 100644 packages/admin/components/layout/Table/bulkActions/hooks.ts
create mode 100644 packages/admin/components/layout/Table/bulkActions/index.ts
create mode 100644 packages/admin/components/layout/Table/bulkActions/reducer.ts
create mode 100644 packages/admin/components/layout/Table/bulkActions/useBulkSelectionBridge.ts
create mode 100644 packages/admin/pages/submissions/publish.tsx
create mode 100644 packages/admin/pages/submissions/review.tsx
diff --git a/packages/admin/__generated__/PublishListFragment.graphql.ts b/packages/admin/__generated__/PublishListFragment.graphql.ts
new file mode 100644
index 000000000..293fe3e9a
--- /dev/null
+++ b/packages/admin/__generated__/PublishListFragment.graphql.ts
@@ -0,0 +1,69 @@
+/**
+ * @generated SignedSource<<287b68080816c737361a83612430fb0b>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { Fragment, ReaderFragment } from 'relay-runtime';
+import { FragmentRefs } from "relay-runtime";
+export type PublishListFragment$data = {
+ readonly nodes: ReadonlyArray<{
+ readonly id: string;
+ readonly slug: string;
+ }>;
+ readonly " $fragmentSpreads": FragmentRefs<"ModelListPageFragment">;
+ readonly " $fragmentType": "PublishListFragment";
+};
+export type PublishListFragment$key = {
+ readonly " $data"?: PublishListFragment$data;
+ readonly " $fragmentSpreads": FragmentRefs<"PublishListFragment">;
+};
+
+const node: ReaderFragment = {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "PublishListFragment",
+ "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 = "e9423f2dfa40344b705194d24dd8ab29";
+
+export default node;
diff --git a/packages/admin/__generated__/publishQuery.graphql.ts b/packages/admin/__generated__/publishQuery.graphql.ts
new file mode 100644
index 000000000..123e61964
--- /dev/null
+++ b/packages/admin/__generated__/publishQuery.graphql.ts
@@ -0,0 +1,216 @@
+/**
+ * @generated SignedSource<<6dc0081450c116665eb8df31a65a2bfa>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+import { FragmentRefs } from "relay-runtime";
+export type publishQuery$variables = {
+ page: number;
+};
+export type publishQuery$data = {
+ readonly viewer: {
+ readonly items: {
+ readonly " $fragmentSpreads": FragmentRefs<"PublishListFragment">;
+ };
+ };
+};
+export type publishQuery = {
+ response: publishQuery$data;
+ variables: publishQuery$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": "publishQuery",
+ "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": "PublishListFragment"
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Operation",
+ "name": "publishQuery",
+ "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": "b0dd93cb74842d2797e2c3ffbaf81183",
+ "id": null,
+ "metadata": {},
+ "name": "publishQuery",
+ "operationKind": "query",
+ "text": "query publishQuery(\n $page: Int!\n) {\n viewer {\n items(page: $page, perPage: 20) {\n ...PublishListFragment\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 PublishListFragment on ItemConnection {\n nodes {\n id\n slug\n }\n ...ModelListPageFragment\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "170bae44e524a3418bd51fe9315ae9d9";
+
+export default node;
diff --git a/packages/admin/__generated__/submissionsQuery.graphql.ts b/packages/admin/__generated__/reviewQuery.graphql.ts
similarity index 81%
rename from packages/admin/__generated__/submissionsQuery.graphql.ts
rename to packages/admin/__generated__/reviewQuery.graphql.ts
index 535b3a490..0be5ed76b 100644
--- a/packages/admin/__generated__/submissionsQuery.graphql.ts
+++ b/packages/admin/__generated__/reviewQuery.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<>
+ * @generated SignedSource<<5b2ea18c1fa5d0514b94bb05109a3241>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,19 +10,19 @@
import { ConcreteRequest, Query } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
-export type submissionsQuery$variables = {
+export type reviewQuery$variables = {
page: number;
};
-export type submissionsQuery$data = {
+export type reviewQuery$data = {
readonly viewer: {
readonly items: {
readonly " $fragmentSpreads": FragmentRefs<"SubmissionListFragment">;
};
};
};
-export type submissionsQuery = {
- response: submissionsQuery$data;
- variables: submissionsQuery$variables;
+export type reviewQuery = {
+ response: reviewQuery$data;
+ variables: reviewQuery$variables;
};
const node: ConcreteRequest = (function(){
@@ -57,7 +57,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
- "name": "submissionsQuery",
+ "name": "reviewQuery",
"selections": [
{
"alias": null,
@@ -94,7 +94,7 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
- "name": "submissionsQuery",
+ "name": "reviewQuery",
"selections": [
{
"alias": null,
@@ -201,16 +201,16 @@ return {
]
},
"params": {
- "cacheID": "a6e461a496e3af441d1fcee9c4330651",
+ "cacheID": "bfc320ac84785a63e845a6ba7f9bdf1b",
"id": null,
"metadata": {},
- "name": "submissionsQuery",
+ "name": "reviewQuery",
"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"
+ "text": "query reviewQuery(\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";
+(node as any).hash = "9d0af5dba293c671a7fc7999ef69b61f";
export default node;
diff --git a/packages/admin/components/atomic/StatusBadge/StatusBadge.tsx b/packages/admin/components/atomic/StatusBadge/StatusBadge.tsx
index 76b19b384..9a2a66938 100644
--- a/packages/admin/components/atomic/StatusBadge/StatusBadge.tsx
+++ b/packages/admin/components/atomic/StatusBadge/StatusBadge.tsx
@@ -7,6 +7,7 @@ const STATUS_COLORS: Record = {
bg: "var(--yellowtint)",
color: "var(--yellowdark)",
},
+ Approved: { bg: "var(--greentint)", color: "var(--greendark)" },
Published: { bg: "var(--greentint)", color: "var(--greendark)" },
};
diff --git a/packages/admin/components/atomic/buttons/ButtonControl/patterns/ButtonControlConfirm.tsx b/packages/admin/components/atomic/buttons/ButtonControl/patterns/ButtonControlConfirm.tsx
index 5476baf52..0ec86c854 100644
--- a/packages/admin/components/atomic/buttons/ButtonControl/patterns/ButtonControlConfirm.tsx
+++ b/packages/admin/components/atomic/buttons/ButtonControl/patterns/ButtonControlConfirm.tsx
@@ -22,6 +22,7 @@ const ButtonControlConfirm = ({
actions,
allowedActions,
loading,
+ disabled,
}: Props) => {
const dialog = useDialogState({ visible: false, animated: true });
@@ -46,6 +47,7 @@ const ButtonControlConfirm = ({
icon={icon}
onClick={handleClick}
aria-label={actionLabel}
+ disabled={disabled}
{...dialog}
>
{children}
diff --git a/packages/admin/components/composed/model/ModelList/ModelList.tsx b/packages/admin/components/composed/model/ModelList/ModelList.tsx
index 1c8248ba7..1a55881ec 100644
--- a/packages/admin/components/composed/model/ModelList/ModelList.tsx
+++ b/packages/admin/components/composed/model/ModelList/ModelList.tsx
@@ -31,20 +31,22 @@ function ModelList<
actions,
disableSortBy,
listId,
+ rowSelection,
+ onRowSelectionChange,
}: ModelListProps) {
const { t } = useTranslation();
const title = modelName ? t(modelName, { count: 2 }) : "";
const queryState = useQueryStateContext();
- // We can also retrieve `selection` from useModelList if we need it, which we eventually
- // will.
const { modelGridOrTableProps } = useModelList({
columns,
actions,
data,
selectable,
disableSortBy,
+ rowSelection,
+ onRowSelectionChange,
});
let ModelListType;
diff --git a/packages/admin/components/composed/model/ModelList/hooks/useModelList.tsx b/packages/admin/components/composed/model/ModelList/hooks/useModelList.tsx
index 807751f48..bc6acdeaf 100644
--- a/packages/admin/components/composed/model/ModelList/hooks/useModelList.tsx
+++ b/packages/admin/components/composed/model/ModelList/hooks/useModelList.tsx
@@ -6,7 +6,11 @@ import { toEntities } from "../helpers/toEntities";
import useTableSorting from "./useTableSorting";
import useRowActions, { Actions } from "./useRowActions";
import useTableRowSelection from "./useTableRowSelection";
-import type { ColumnDef } from "@tanstack/react-table";
+import type {
+ ColumnDef,
+ OnChangeFn,
+ RowSelectionState,
+} from "@tanstack/react-table";
/* eslint-disable @typescript-eslint/no-unused-vars */
export interface UseModelListProps<
@@ -19,6 +23,10 @@ export interface UseModelListProps<
selectable?: boolean;
/** Disable sorting on all columns */
disableSortBy?: boolean;
+ /** Controlled row selection state (overrides internal state) */
+ rowSelection?: RowSelectionState;
+ /** Controlled row selection change handler */
+ onRowSelectionChange?: OnChangeFn;
}
function useModelList<
@@ -30,6 +38,8 @@ function useModelList<
actions = {},
selectable = false,
disableSortBy,
+ rowSelection: controlledRowSelection,
+ onRowSelectionChange: controlledOnRowSelectionChange,
}: UseModelListProps) {
// Extract entities from the connectionish data
const entities = useMemo(() => toEntities(data), [data]);
@@ -42,8 +52,9 @@ function useModelList<
// Set up sorting
const [sorting, setSorting] = useTableSorting();
- // Set up row selection
- const [rowSelection, setRowSelection] = useTableRowSelection();
+ // Set up row selection (internal state, used when not controlled)
+ const [internalRowSelection, setInternalRowSelection] =
+ useTableRowSelection();
// Get the columns with actions
const allColumns = useRowActions(columns, actions);
@@ -55,10 +66,11 @@ function useModelList<
getRowId,
state: {
sorting,
- rowSelection,
+ rowSelection: controlledRowSelection ?? internalRowSelection,
},
onSortingChange: setSorting,
- onRowSelectionChange: setRowSelection,
+ onRowSelectionChange:
+ controlledOnRowSelectionChange ?? setInternalRowSelection,
enableMultisort: false,
enableSorting: !disableSortBy,
enableRowSelection: selectable,
diff --git a/packages/admin/components/composed/model/ModelList/hooks/useRowActions.tsx b/packages/admin/components/composed/model/ModelList/hooks/useRowActions.tsx
index 367ab6729..1d1e93df1 100644
--- a/packages/admin/components/composed/model/ModelList/hooks/useRowActions.tsx
+++ b/packages/admin/components/composed/model/ModelList/hooks/useRowActions.tsx
@@ -23,6 +23,8 @@ type ActionConfig> =
| {
handleClick: ({ row }: { row: Row }) => void;
modalConfirm?: boolean;
+ modalLabel?: ({ row }: { row: Row }) => string;
+ modalBody?: ({ row }: { row: Row }) => React.ReactNode;
handleHide?: ({ row }: { row: Row }) => boolean;
handleLink?: ({ row }: { row: Row }) => string | void;
}
@@ -41,12 +43,14 @@ type ActionKeys =
| "purge"
| "view"
| "enable"
- | "disable";
+ | "disable"
+ | "publish";
// Change the sort order of action buttons here
const ACTION_ORDER = [
"download",
"view",
+ "publish",
"edit",
"enable",
"disable",
@@ -112,6 +116,10 @@ const availableActions: ActionDefinitions = {
{i18next.t("messages.disable.confirm_body")}
),
},
+ publish: {
+ label: i18next.t("nav.publish"),
+ modalLabel: i18next.t("actions.publish_confirm_label"),
+ },
};
function getButtonControlChildren>(
@@ -132,6 +140,13 @@ function getButtonControlChildren>(
if (actionConfig?.handleHide && actionConfig?.handleHide({ row }))
return ;
+ const dynamicModalLabel = actionConfig?.modalLabel
+ ? actionConfig.modalLabel({ row })
+ : actionDefinition.modalLabel;
+ const dynamicModalBody = actionConfig?.modalBody
+ ? actionConfig.modalBody({ row })
+ : actionDefinition.modalBody ?? null;
+
const buttonControl = actionConfig?.modalConfirm ? (
>(
if (hideDialog) hideDialog();
},
})}
- modalLabel={actionDefinition.modalLabel}
- modalBody={actionDefinition.modalBody ?? null}
+ modalLabel={dynamicModalLabel}
+ modalBody={dynamicModalBody}
disabled={disableDelete && action === "delete"}
>
{actionDefinition.icon ? null : actionDefinition.label}
@@ -247,6 +262,8 @@ export interface Actions> {
hideEnable?: (props: ModelTableActionProps) => boolean;
handleDisable?: (props: ModelTableActionProps) => void;
hideDisable?: (props: ModelTableActionProps) => boolean;
+ handlePublish?: (props: ModelTableActionProps) => void;
+ publishModalBody?: (props: ModelTableActionProps) => React.ReactNode;
}
function useRowActions>(
@@ -304,6 +321,13 @@ function useRowActions>(
modalConfirm: true,
},
}),
+ ...(actions.handlePublish && {
+ publish: {
+ handleClick: actions.handlePublish,
+ modalConfirm: true,
+ modalBody: actions.publishModalBody,
+ },
+ }),
}),
[actions, renderedDelete],
);
diff --git a/packages/admin/components/composed/model/ModelListPage/ModelListPage.tsx b/packages/admin/components/composed/model/ModelListPage/ModelListPage.tsx
index 4f75bbb61..1cbe59d07 100644
--- a/packages/admin/components/composed/model/ModelListPage/ModelListPage.tsx
+++ b/packages/admin/components/composed/model/ModelListPage/ModelListPage.tsx
@@ -28,13 +28,21 @@ type ModelListPageProps<
V extends Record = Record,
> = Omit, "view"> &
Pick &
- Pick & {
+ Pick<
+ HeaderProps,
+ "headerStyle" | "hideHeader" | "tabRoutes" | "tabLinksOnly"
+ > & {
buttons?: React.ReactNode;
header?: string | null;
showSearch?: boolean;
hideFilters?: boolean;
searchData?: ModelListPageSearchFragment$key | null;
- } & { countActions?: React.JSX.Element };
+ countActions?: React.JSX.Element;
+ /** Number of selected items, passed to PageCountActions */
+ selectedCount?: number;
+ /** Rendered between count row and table (e.g. SelectAll bar) */
+ selectionBar?: React.ReactNode;
+ };
function ModelListPage<
U extends PaginatedConnectionish,
@@ -46,11 +54,15 @@ function ModelListPage<
headerStyle,
hideHeader,
header,
+ tabRoutes,
+ tabLinksOnly,
showSearch,
hideFilters,
data,
searchData,
countActions,
+ selectedCount,
+ selectionBar,
...modelListProps
}: ModelListPageProps) {
const { t } = useTranslation();
@@ -92,6 +104,8 @@ function ModelListPage<
buttons={buttons}
headerStyle={headerStyle}
hideHeader={hideHeader}
+ tabRoutes={tabRoutes}
+ tabLinksOnly={tabLinksOnly}
/>
{searchScope && }
-
+
+ {selectionBar}
{...modelListProps}
data={data}
diff --git a/packages/admin/components/composed/model/ModelPageCountActions/ModelPageCountActions.tsx b/packages/admin/components/composed/model/ModelPageCountActions/ModelPageCountActions.tsx
index 47edf954d..36fef0094 100644
--- a/packages/admin/components/composed/model/ModelPageCountActions/ModelPageCountActions.tsx
+++ b/packages/admin/components/composed/model/ModelPageCountActions/ModelPageCountActions.tsx
@@ -8,11 +8,13 @@ interface ModelPageCountActionsProps<
> {
data?: T | null;
actions?: React.JSX.Element;
+ selectedCount?: number;
}
function ModelPageCountActions({
data,
actions,
+ selectedCount,
}: ModelPageCountActionsProps) {
const enhancedData = useMaybeFragment(
fragment,
@@ -24,6 +26,7 @@ function ModelPageCountActions({
loading={typeof enhancedData?.pageInfo.totalCount !== "number"}
pageInfo={enhancedData?.pageInfo}
actions={actions}
+ selectedCount={selectedCount}
/>
);
}
diff --git a/packages/admin/components/composed/submission/PublishList/PublishList.tsx b/packages/admin/components/composed/submission/PublishList/PublishList.tsx
new file mode 100644
index 000000000..f2e0f3d68
--- /dev/null
+++ b/packages/admin/components/composed/submission/PublishList/PublishList.tsx
@@ -0,0 +1,152 @@
+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 { ButtonControlConfirm } from "components/atomic";
+import {
+ useBulkActions,
+ useBulkSelectionBridge,
+} from "components/layout/Table/bulkActions";
+import type {
+ PublishListFragment$data,
+ PublishListFragment$key,
+} from "@/relay/PublishListFragment.graphql";
+import {
+ MOCK_SUBMISSIONS,
+ type SubmissionNode,
+} from "../SubmissionList/mockData";
+import type { ModelTableActionProps } from "@tanstack/react-table";
+
+type Props = {
+ data?: PublishListFragment$key;
+};
+
+function PublishList({ data }: Props) {
+ const connection = useFragment(fragment, data);
+
+ const { t } = useTranslation();
+
+ const filteredSubmissions = MOCK_SUBMISSIONS.filter(
+ (s) => s.status === "Approved",
+ );
+
+ const dataWithMockNodes = connection
+ ? ({
+ ...connection,
+ nodes: filteredSubmissions,
+ } as PublishListFragment$data)
+ : undefined;
+
+ // --- Bulk actions ---
+ const records = filteredSubmissions;
+ const filters = {};
+
+ const { bulkSelection, addItem, removeItem, addPage } = useBulkActions(
+ records,
+ filters,
+ );
+
+ const { rowSelection, onRowSelectionChange } = useBulkSelectionBridge(
+ records,
+ bulkSelection,
+ addItem,
+ removeItem,
+ addPage,
+ );
+
+ const allMatchingSelected = !!bulkSelection.filters;
+ const selectedCount = allMatchingSelected
+ ? filteredSubmissions.length
+ : bulkSelection.ids.length;
+
+ const handleBulkPublish = (hideDialog: () => void) => {
+ // TODO: wire mutation — send bulkSelection.filters or bulkSelection.ids
+ // eslint-disable-next-line no-console
+ console.log("Bulk publish:", bulkSelection);
+ hideDialog();
+ };
+
+ // --- Columns ---
+ const columns = [
+ ModelColumns.NameColumn({
+ accessor: "title",
+ route: "submissions.detail",
+ enableSorting: false,
+ }),
+ ModelColumns.StringColumn({
+ id: "collection",
+ header: () => t("lists.collection_column"),
+ }),
+ ModelColumns.UpdatedAtColumn({
+ enableSorting: false,
+ }),
+ ];
+
+ const actions = {
+ handleView: ({ row }: ModelTableActionProps) =>
+ row.original.slug ? `/submissions/${row.original.slug}/details` : null,
+ handlePublish: ({ row }: ModelTableActionProps) => {
+ // TODO: wire mutation for single publish
+ // eslint-disable-next-line no-console
+ console.log("Publish:", row.original.id);
+ },
+ publishModalBody: ({ row }: ModelTableActionProps) => (
+
+ {t("actions.publish_confirm_body_single", {
+ title: row.original.title,
+ })}
+
+ ),
+ };
+
+ const tabRoutes = [
+ { route: "submissions.review", label: t("nav.review") },
+ { route: "submissions.publish", label: t("nav.publish") },
+ ];
+
+ return (
+
+ modelName="submission"
+ header={t("nav.submissions_header")}
+ columns={columns}
+ data={dataWithMockNodes}
+ actions={actions}
+ tabRoutes={tabRoutes}
+ tabLinksOnly
+ showSearch
+ hideFilters
+ disableSortBy
+ selectable
+ rowSelection={rowSelection}
+ onRowSelectionChange={onRowSelectionChange}
+ selectedCount={selectedCount}
+ countActions={
+
+ {t("actions.publish_confirm_body", { count: selectedCount })}
+
+ }
+ >
+ {t("actions.bulk_publish")}
+
+ }
+ />
+ );
+}
+
+const fragment = graphql`
+ fragment PublishListFragment on ItemConnection {
+ nodes {
+ id
+ slug
+ }
+ ...ModelListPageFragment
+ }
+`;
+
+export default PublishList;
diff --git a/packages/admin/components/composed/submission/PublishList/index.tsx b/packages/admin/components/composed/submission/PublishList/index.tsx
new file mode 100644
index 000000000..183d6dcdb
--- /dev/null
+++ b/packages/admin/components/composed/submission/PublishList/index.tsx
@@ -0,0 +1 @@
+export { default } from "./PublishList";
diff --git a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx
index 5f091c1f5..d0e28a383 100644
--- a/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx
+++ b/packages/admin/components/composed/submission/SubmissionList/SubmissionList.tsx
@@ -13,23 +13,26 @@ import type { ModelTableActionProps } from "@tanstack/react-table";
type Props = {
data?: SubmissionListFragment$key;
+ mode?: "review" | "my-submissions";
};
-function SubmissionList({ data }: Props) {
+function SubmissionList({ data, mode = "review" }: Props) {
const connection = useFragment(fragment, data);
const { t } = useTranslation();
const router = useRouter();
- const isReview = router.pathname.startsWith("/submissions");
- const detailRoute = isReview ? "submissions.detail" : "my-submissions.detail";
+ const isMySubmissions = mode === "my-submissions";
+ const detailRoute = isMySubmissions
+ ? "my-submissions.detail"
+ : "submissions.detail";
+ const basePath = isMySubmissions ? "my-submissions" : "submissions";
- /* Replace real nodes with mock data while preserving pagination fragment refs */
- const filteredSubmissions = isReview
- ? MOCK_SUBMISSIONS.filter(
+ const filteredSubmissions = isMySubmissions
+ ? MOCK_SUBMISSIONS
+ : MOCK_SUBMISSIONS.filter(
(s) => s.status === "In Review" || s.status === "Revisions Requested",
- )
- : MOCK_SUBMISSIONS;
+ );
const dataWithMockNodes = connection
? ({
@@ -59,33 +62,40 @@ function SubmissionList({ data }: Props) {
const actions = {
handleEdit: ({ row }: ModelTableActionProps) => {
if (row.original.slug) {
- router.push(
- `/${isReview ? "submissions" : "my-submissions"}/${row.original.slug}/details/edit`,
- );
+ router.push(`/${basePath}/${row.original.slug}/details/edit`);
}
},
handleView: ({ row }: ModelTableActionProps) =>
- row.original.slug
- ? `/${isReview ? "submissions" : "my-submissions"}/${row.original.slug}/details`
- : null,
+ row.original.slug ? `/${basePath}/${row.original.slug}/details` : null,
};
- const buttons = (
+ const buttons = isMySubmissions ? (
{t("actions.add.submission")}
- );
+ ) : undefined;
+
+ const tabRoutes = !isMySubmissions
+ ? [
+ { route: "submissions.review", label: t("nav.review") },
+ { route: "submissions.publish", label: t("nav.publish") },
+ ]
+ : undefined;
return (
modelName="submission"
- header={isReview ? t("nav.review_submissions") : t("nav.my_submissions")}
+ header={t(
+ isMySubmissions ? "nav.my_submissions" : "nav.submissions_header",
+ )}
columns={columns}
data={dataWithMockNodes}
actions={actions}
buttons={buttons}
+ tabRoutes={tabRoutes}
+ tabLinksOnly
showSearch
hideFilters
disableSortBy
diff --git a/packages/admin/components/composed/submission/SubmissionList/mockData.ts b/packages/admin/components/composed/submission/SubmissionList/mockData.ts
index 693afaa8a..80b18f1ec 100644
--- a/packages/admin/components/composed/submission/SubmissionList/mockData.ts
+++ b/packages/admin/components/composed/submission/SubmissionList/mockData.ts
@@ -58,4 +58,22 @@ export const MOCK_SUBMISSIONS: readonly SubmissionNode[] = [
updatedAt: "2026-02-10T08:30:00Z",
allowedActions: ["self.update"],
},
+ {
+ id: "mock-submission-6",
+ slug: "mock-submission-6",
+ title: "Sustainable Funding Models for Open Repositories",
+ status: "Approved",
+ collection: "Library Science Research",
+ updatedAt: "2026-02-25T10:00:00Z",
+ allowedActions: ["self.update"],
+ },
+ {
+ id: "mock-submission-7",
+ slug: "mock-submission-7",
+ title: "Interoperability Standards for Digital Archives",
+ status: "Approved",
+ collection: "Archives & Records",
+ updatedAt: "2026-02-27T13:45:00Z",
+ allowedActions: ["self.update"],
+ },
];
diff --git a/packages/admin/components/layout/PageCountActions/PageCountActions.tsx b/packages/admin/components/layout/PageCountActions/PageCountActions.tsx
index 76d317d5e..e1892b9e6 100644
--- a/packages/admin/components/layout/PageCountActions/PageCountActions.tsx
+++ b/packages/admin/components/layout/PageCountActions/PageCountActions.tsx
@@ -14,7 +14,7 @@ const PageCountActions = ({
loading,
}: Props) => {
const { t } = useTranslation();
- const hasSelected = selectedCount > 1;
+ const hasSelected = selectedCount > 0;
const page = pageInfo?.page || 1;
const perPage = pageInfo?.perPage || 10;
const totalCount = pageInfo?.totalCount || 0;
@@ -29,25 +29,35 @@ const PageCountActions = ({
{loading ? (
t("common.loading_ellipsis")
- ) : hasSelected ? (
- ]}
- />
) : (
- ]}
- />
+ <>
+ ,
+ ]}
+ />
+ {hasSelected && (
+ <>
+ {" · "}
+ ,
+ ]}
+ />
+ >
+ )}
+ >
)}
{actions ? {actions} : null}
diff --git a/packages/admin/components/layout/Table/Table.styles.ts b/packages/admin/components/layout/Table/Table.styles.ts
index 95f48b137..b032b7ff0 100644
--- a/packages/admin/components/layout/Table/Table.styles.ts
+++ b/packages/admin/components/layout/Table/Table.styles.ts
@@ -10,19 +10,20 @@ export const TableWrapper = styled.div`
--table-margin-left: ${pxToRem(8)};
--table-margin-right: ${pxToRem(8)};
--checkbox-opacity: 0;
- --checkbox-visibility: 0;
+ --checkbox-visibility: hidden;
${({ $selectable }) =>
$selectable &&
css`
- --table-margin-left: ${pxToRem(52)};
+ --checkbox-opacity: 1;
+ --checkbox-visibility: visible;
`}
${({ $showCheckboxes }) =>
$showCheckboxes &&
css`
--checkbox-opacity: 1;
- --checkbox-visibility: 1;
+ --checkbox-visibility: visible;
`}
border-top: var(--table-border);
@@ -58,11 +59,6 @@ export const HeaderCell = styled.th`
cursor: pointer;
}
- &[data-select-cell="true"] {
- padding-inline-start: var(--table-column-gap);
- border-bottom: 0;
- }
-
&[role="presentation"] {
border-bottom: 0;
}
@@ -135,17 +131,21 @@ export const Cell = styled.td`
`;
export const SelectCell = styled.td`
- width: 0.1%;
- max-width: auto;
white-space: nowrap;
padding: ${pxToRem(14)} var(--table-column-gap) 0;
- border-top-left-radius: var(--table-border-radius);
- border-bottom-left-radius: var(--table-border-radius);
+ text-align: center;
`;
export const SelectCellInner = styled.div`
visibility: var(--checkbox-visibility);
opacity: var(--checkbox-opacity);
+ display: flex;
+ justify-content: center;
+ margin-block-start: ${pxToRem(4)};
+
+ th & {
+ margin-block-start: 0;
+ }
`;
export const LoadingRow = styled.tr``;
@@ -169,8 +169,6 @@ export const Row = styled.tr`
--button-control-opacity: 1;
--button-control-visibility: 1;
- --checkbox-opacity: 1;
- --checkbox-visibility: 1;
}
`;
@@ -202,7 +200,5 @@ export const HeaderRow = styled.tr`
&:focus-within {
--button-control-opacity: 1;
--button-control-visibility: 1;
- --checkbox-opacity: 1;
- --checkbox-visibility: 1;
}
`;
diff --git a/packages/admin/components/layout/Table/TableHeader.tsx b/packages/admin/components/layout/Table/TableHeader.tsx
index 24ca19d1c..1b86de46b 100644
--- a/packages/admin/components/layout/Table/TableHeader.tsx
+++ b/packages/admin/components/layout/Table/TableHeader.tsx
@@ -1,5 +1,6 @@
import React, { useEffect } from "react";
import { flexRender } from "@tanstack/react-table";
+import { useTranslation } from "react-i18next";
import { Checkbox } from "components/forms";
import TableHeaderRow from "./TableHeaderRow";
import * as Styled from "./Table.styles";
@@ -17,6 +18,7 @@ function TableHeader>({
/* eslint-disable react/jsx-key */
/* keys are injected using the get props functions */
const { setColumnCount } = useTableContext();
+ const { t } = useTranslation();
useEffect(() => {
if (headerGroups.length > 0 && setColumnCount) {
@@ -32,9 +34,10 @@ function TableHeader>({
return (
{selectable ? (
-
+
span {
+ ${tLabel("sm")}
+ font-size: 13px;
+ color: var(--strong-color);
+ margin-inline-end: 12px;
+ display: inline-block;
+
+ &::before {
+ content: "";
+ display: inline-block;
+ height: 8px;
+ width: 16px;
+ border-bottom-left-radius: 4px;
+ border-left: 1px solid var(--strong-color);
+ border-bottom: 1px solid var(--strong-color);
+ color: var(--color-neutral-ui-light);
+ margin-inline-end: 12px;
+ }
+ }
+`;
diff --git a/packages/admin/components/layout/Table/bulkActions/SelectAll/SelectAll.tsx b/packages/admin/components/layout/Table/bulkActions/SelectAll/SelectAll.tsx
new file mode 100644
index 000000000..ca76c9df1
--- /dev/null
+++ b/packages/admin/components/layout/Table/bulkActions/SelectAll/SelectAll.tsx
@@ -0,0 +1,96 @@
+import { useTranslation, Trans } from "react-i18next";
+import * as Styled from "./SelectAll.styles";
+
+interface Pagination {
+ currentPage: number;
+ perPage: number;
+ totalCount: number;
+ totalPages?: number;
+}
+
+interface Props {
+ pagination: Pagination;
+ unit: string;
+ onSelect?: () => void;
+ onClear?: () => void;
+ onSelectPage?: () => void;
+ allSelected: boolean;
+ idsSelectedCount: number;
+}
+
+function getRangeValues(pagination: Pagination) {
+ const start = (pagination.currentPage - 1) * pagination.perPage + 1;
+ const end = Math.min(
+ pagination.currentPage * pagination.perPage,
+ pagination.totalCount,
+ );
+ return { start, end, totalCount: pagination.totalCount };
+}
+
+export default function SelectAll({
+ pagination,
+ unit,
+ onSelect,
+ onClear,
+ onSelectPage,
+ allSelected,
+ idsSelectedCount,
+}: Props) {
+ const { t } = useTranslation();
+
+ const hasSelection = allSelected || !!idsSelectedCount;
+
+ const { start, end, totalCount } = getRangeValues(pagination);
+ const pageCount = end - start + 1;
+ const unpaginated = pagination?.totalPages === 1;
+
+ // Valid because page change resets bulk selection
+ const allPageSelected = idsSelectedCount === pageCount;
+
+ const showActions =
+ (!allPageSelected && !allSelected) || (hasSelection && !allSelected);
+
+ return (
+ <>
+ {showActions && (
+
+ {!allPageSelected && !allSelected && (
+
+ {t("actions.select_all")}
+
+ )}
+ {hasSelection && !allSelected && (
+
+ {t("actions.clear_selection")}
+
+ )}
+
+ )}
+ {!unpaginated && (allPageSelected || allSelected) && (
+
+
+ ]}
+ />
+
+ {!allSelected ? (
+
+ {t("actions.select_all_matching", { count: totalCount, unit })}
+
+ ) : (
+
+ {t("actions.clear_selection")}
+
+ )}
+
+ )}
+ >
+ );
+}
+
+SelectAll.displayName = "Table.BulkActions.SelectAll";
diff --git a/packages/admin/components/layout/Table/bulkActions/SelectAll/index.ts b/packages/admin/components/layout/Table/bulkActions/SelectAll/index.ts
new file mode 100644
index 000000000..a2f7dfd7b
--- /dev/null
+++ b/packages/admin/components/layout/Table/bulkActions/SelectAll/index.ts
@@ -0,0 +1 @@
+export { default } from "./SelectAll";
diff --git a/packages/admin/components/layout/Table/bulkActions/hooks.ts b/packages/admin/components/layout/Table/bulkActions/hooks.ts
new file mode 100644
index 000000000..3cd8686f0
--- /dev/null
+++ b/packages/admin/components/layout/Table/bulkActions/hooks.ts
@@ -0,0 +1,133 @@
+import {
+ useState,
+ useReducer,
+ useMemo,
+ useCallback,
+ useEffect,
+ useRef,
+} from "react";
+import { useRouter } from "next/router";
+import isEqual from "lodash/isEqual";
+import { bulkActionsReducer, type BulkSelectionState } from "./reducer";
+
+interface BulkActionsReturn {
+ bulkActionsActive: boolean;
+ toggleBulkActions: () => void;
+ resetBulkSelection: () => void;
+ handleSelectAll: () => void;
+ handleSelectAllUncheck: (removeId: string) => void;
+ bulkSelection: BulkSelectionState;
+ bulkSelectionEmpty: boolean;
+ addItem: (id: string) => void;
+ removeItem: (id: string) => void;
+ addPage: (ids: string[]) => void;
+}
+
+export default function useBulkActions(
+ records: { id: string }[] | undefined,
+ filters: Record,
+): BulkActionsReturn {
+ const [bulkActionsActive, setBulkActionsActive] = useState(false);
+
+ const toggleBulkActions = useCallback(
+ () => setBulkActionsActive(!bulkActionsActive),
+ [bulkActionsActive, setBulkActionsActive],
+ );
+
+ const initSelectionState = useMemo(
+ () => ({
+ ids: [],
+ filters: null,
+ }),
+ [],
+ );
+
+ const [bulkSelection, dispatchSelection] = useReducer(
+ bulkActionsReducer,
+ initSelectionState,
+ );
+
+ const bulkSelectionEmpty = isEqual(initSelectionState, bulkSelection);
+
+ const visibleIds = useMemo(() => records?.map((a) => a.id), [records]);
+
+ const handleSelectAll = useCallback(() => {
+ const { order, ...selectionFilters } = filters;
+ return dispatchSelection({
+ type: "setFilters",
+ payload: selectionFilters,
+ });
+ }, [filters]);
+
+ const handleSelectAllUncheck = (removeId: string) => {
+ dispatchSelection({
+ type: "removeAndClear",
+ payload: visibleIds?.filter((id) => id !== removeId) ?? [],
+ });
+ };
+
+ const resetBulkSelection = useCallback(
+ () =>
+ dispatchSelection({
+ type: "reset",
+ payload: initSelectionState,
+ }),
+ [initSelectionState],
+ );
+
+ const addItem = (id: string) =>
+ dispatchSelection({ type: "add", payload: id });
+ const removeItem = (id: string) =>
+ bulkSelection?.filters
+ ? handleSelectAllUncheck(id)
+ : dispatchSelection({ type: "remove", payload: id });
+ const addPage = (ids: string[]) =>
+ dispatchSelection({ type: "addPage", payload: ids });
+
+ const router = useRouter();
+ const page = (router.query.page as string) ?? "1";
+ const pageRef = useRef(page);
+
+ useEffect(() => {
+ if (page && page !== pageRef.current) {
+ resetBulkSelection();
+ }
+ }, [page, resetBulkSelection]);
+
+ return {
+ bulkActionsActive,
+ toggleBulkActions,
+ resetBulkSelection,
+ handleSelectAll,
+ handleSelectAllUncheck,
+ bulkSelection,
+ bulkSelectionEmpty,
+ addItem,
+ removeItem,
+ addPage,
+ };
+}
+
+export const useClearBulkSelectionWithFilters = (
+ baseOnReset: () => void,
+ baseSetFilter: (param: string, value: unknown) => void,
+ resetBulkSelection: () => void,
+ bulkSelectionEmpty: boolean,
+) => {
+ const setParam = useCallback(
+ (param: string, value: unknown) => {
+ baseSetFilter(param, value);
+
+ if (!bulkSelectionEmpty) resetBulkSelection();
+ },
+ [bulkSelectionEmpty, resetBulkSelection, baseSetFilter],
+ );
+
+ const onReset = useCallback(() => {
+ baseOnReset();
+
+ if (!bulkSelectionEmpty) resetBulkSelection();
+ }, [bulkSelectionEmpty, resetBulkSelection, baseOnReset]);
+
+ return { setParam, onReset };
+};
diff --git a/packages/admin/components/layout/Table/bulkActions/index.ts b/packages/admin/components/layout/Table/bulkActions/index.ts
new file mode 100644
index 000000000..14c593fab
--- /dev/null
+++ b/packages/admin/components/layout/Table/bulkActions/index.ts
@@ -0,0 +1,10 @@
+import SelectAll from "./SelectAll";
+import useBulkActions, { useClearBulkSelectionWithFilters } from "./hooks";
+import useBulkSelectionBridge from "./useBulkSelectionBridge";
+
+export {
+ SelectAll,
+ useBulkActions,
+ useBulkSelectionBridge,
+ useClearBulkSelectionWithFilters,
+};
diff --git a/packages/admin/components/layout/Table/bulkActions/reducer.ts b/packages/admin/components/layout/Table/bulkActions/reducer.ts
new file mode 100644
index 000000000..4d108313e
--- /dev/null
+++ b/packages/admin/components/layout/Table/bulkActions/reducer.ts
@@ -0,0 +1,34 @@
+export interface BulkSelectionState {
+ ids: string[];
+ filters: Record | null;
+}
+
+export type BulkAction =
+ | { type: "add"; payload: string }
+ | { type: "remove"; payload: string }
+ | { type: "addPage"; payload: string[] }
+ | { type: "removeAndClear"; payload: string[] }
+ | { type: "setFilters"; payload: Record }
+ | { type: "reset"; payload: BulkSelectionState };
+
+export function bulkActionsReducer(
+ state: BulkSelectionState,
+ action: BulkAction,
+): BulkSelectionState {
+ switch (action.type) {
+ case "add":
+ return { ...state, ids: [...state.ids, action.payload] };
+ case "remove":
+ return { ...state, ids: state.ids.filter((id) => id !== action.payload) };
+ case "addPage":
+ return { filters: null, ids: action.payload };
+ case "removeAndClear":
+ return { filters: null, ids: action.payload };
+ case "setFilters":
+ return { ids: [], filters: action.payload };
+ case "reset":
+ return action.payload;
+ default:
+ return state;
+ }
+}
diff --git a/packages/admin/components/layout/Table/bulkActions/useBulkSelectionBridge.ts b/packages/admin/components/layout/Table/bulkActions/useBulkSelectionBridge.ts
new file mode 100644
index 000000000..3b8474641
--- /dev/null
+++ b/packages/admin/components/layout/Table/bulkActions/useBulkSelectionBridge.ts
@@ -0,0 +1,58 @@
+import { useCallback, useMemo } from "react";
+import type { OnChangeFn, RowSelectionState } from "@tanstack/react-table";
+import type { BulkSelectionState } from "./reducer";
+
+/**
+ * Bridges useBulkActions state ↔ react-table RowSelectionState.
+ *
+ * Derives `rowSelection` from the bulk selection and translates
+ * react-table checkbox changes back into bulk action dispatches.
+ */
+export default function useBulkSelectionBridge(
+ records: { id: string }[] | undefined,
+ bulkSelection: BulkSelectionState,
+ addItem: (id: string) => void,
+ removeItem: (id: string) => void,
+ addPage: (ids: string[]) => void,
+) {
+ // Derive RowSelectionState from bulkSelection
+ const rowSelection = useMemo(() => {
+ if (bulkSelection.filters) {
+ // Filter mode: all visible rows selected
+ return (records ?? []).reduce(
+ (acc, r) => ({ ...acc, [r.id]: true }),
+ {},
+ );
+ }
+ return bulkSelection.ids.reduce(
+ (acc, id) => ({ ...acc, [id]: true }),
+ {},
+ );
+ }, [bulkSelection, records]);
+
+ // Handle changes from react-table checkboxes
+ const onRowSelectionChange: OnChangeFn = useCallback(
+ (updaterOrValue) => {
+ const newState =
+ typeof updaterOrValue === "function"
+ ? updaterOrValue(rowSelection)
+ : updaterOrValue;
+
+ const newIds = Object.keys(newState).filter((id) => newState[id]);
+ const oldIds = Object.keys(rowSelection).filter((id) => rowSelection[id]);
+ const added = newIds.filter((id) => !oldIds.includes(id));
+ const removed = oldIds.filter((id) => !newIds.includes(id));
+
+ // Header "select all" checkbox toggles all page rows at once
+ if (added.length > 1 && removed.length === 0) {
+ addPage(newIds);
+ } else {
+ added.forEach(addItem);
+ removed.forEach(removeItem);
+ }
+ },
+ [rowSelection, addItem, removeItem, addPage],
+ );
+
+ return { rowSelection, onRowSelectionChange };
+}
diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json
index e67ab32dd..cbe51b632 100644
--- a/packages/admin/locales/en.json
+++ b/packages/admin/locales/en.json
@@ -129,7 +129,7 @@
"no_results": "No Results",
"contribution_count": "{{count}} contribution",
"contribution_count_other": "{{count}} contributions",
- "selected_count": "<0>Selected: {{count}}0> of <0>{{total}}0>",
+ "selected_count": "<0>{{count}}0> Selected",
"showing_count": "Showing <0>{{start}} - {{end}}0> of <0>{{total}}0>",
"sort": {
"label": "Sort",
@@ -199,6 +199,10 @@
"permalinks": "Permalinks",
"my_submissions": "My Submissions",
"review_submissions": "Review Submissions",
+ "submissions_header": "Submissions",
+ "publish_submissions": "Publish Submissions",
+ "review": "Review",
+ "publish": "Publish",
"new_submission": "New Submission",
"history": "History"
},
@@ -425,7 +429,16 @@
"submit_for_review": "Submit",
"submit_for_review_confirm": "Are you sure you're ready to submit this for review? It will be sent to a reviewer for evaluation.",
"resubmit_for_review": "Resubmit",
- "resubmit_for_review_confirm": "Are you sure you're ready to resubmit this for review? It will be sent back to a reviewer for evaluation."
+ "resubmit_for_review_confirm": "Are you sure you're ready to resubmit this for review? It will be sent back to a reviewer for evaluation.",
+ "bulk_publish": "Bulk Publish",
+ "publish_confirm_label": "Confirm Publish",
+ "publish_confirm_body": "Are you sure you want to publish the {{count}} selected submission(s)?",
+ "publish_confirm_body_single": "Are you sure you want to publish \"{{title}}\"?",
+ "bulk_delete": "Delete",
+ "cancel": "Cancel",
+ "select_all": "Select all",
+ "clear_selection": "Clear selection",
+ "select_all_matching": "Select all {{count}} {{unit}}"
},
"sort": {
"ascending": "ascending",
@@ -587,7 +600,8 @@
"status": "Status",
"last_updated": "Last Updated",
"contributors": "Contributors",
- "files": "Files"
+ "files": "Files",
+ "accepts_submissions": "This collection accepts submissions"
},
"validation": {
"year": "Must be a four digit year",
@@ -751,6 +765,15 @@
"mapping_details_column_set": "{{format}} to {{target}} ({{set}})",
"view_target": "View Target",
"mapping_breadcrumb": "{{format}} Mapping to {{target}}"
+ },
+ "counts": {
+ "range": "<0>{{start}}–{{end}}0> of {{total}} {{unit}}",
+ "bulk_actions_match": "<0>{{total}}0> {{unit}} selected"
+ },
+ "records": {
+ "annotations": {
+ "bulk_actions": "Bulk Actions"
+ }
}
}
}
diff --git a/packages/admin/pages/submissions/index.tsx b/packages/admin/pages/submissions/index.tsx
index 73aa41b29..f7297a7c2 100644
--- a/packages/admin/pages/submissions/index.tsx
+++ b/packages/admin/pages/submissions/index.tsx
@@ -1,44 +1,8 @@
-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";
+import { Page } from "@wdp/lib/types/page";
-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 ;
+// This is a redirect route. See routes/baseRoutes.ts.
+const Submissions: Page = () => {
+ return null;
};
-const query = graphql`
- query submissionsQuery($page: Int!) {
- viewer {
- items(page: $page, perPage: 20) {
- ...SubmissionListFragment
- }
- }
- }
-`;
+export default Submissions;
diff --git a/packages/admin/pages/submissions/publish.tsx b/packages/admin/pages/submissions/publish.tsx
new file mode 100644
index 000000000..03a0196aa
--- /dev/null
+++ b/packages/admin/pages/submissions/publish.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 PublishList from "components/composed/submission/PublishList";
+import HtmlHead from "components/global/HtmlHead";
+import { publishQuery as Query } from "@/relay/publishQuery.graphql";
+
+export default function SubmissionsPublish() {
+ 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 publishQuery($page: Int!) {
+ viewer {
+ items(page: $page, perPage: 20) {
+ ...PublishListFragment
+ }
+ }
+ }
+`;
diff --git a/packages/admin/pages/submissions/review.tsx b/packages/admin/pages/submissions/review.tsx
new file mode 100644
index 000000000..309299fe2
--- /dev/null
+++ b/packages/admin/pages/submissions/review.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 { reviewQuery as Query } from "@/relay/reviewQuery.graphql";
+
+export default function SubmissionsReview() {
+ 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 reviewQuery($page: Int!) {
+ viewer {
+ items(page: $page, perPage: 20) {
+ ...SubmissionListFragment
+ }
+ }
+ }
+`;
diff --git a/packages/admin/routes/baseRoutes.ts b/packages/admin/routes/baseRoutes.ts
index b00e075c8..9311696eb 100644
--- a/packages/admin/routes/baseRoutes.ts
+++ b/packages/admin/routes/baseRoutes.ts
@@ -299,8 +299,19 @@ export const baseRoutes: BaseRoute[] = [
{
name: "submissions",
path: "/submissions",
+ redirect: "/submissions/review",
label: "glossary.submission_other",
routes: [
+ {
+ name: "submissions.review",
+ path: "/submissions/review",
+ label: "nav.review",
+ },
+ {
+ name: "submissions.publish",
+ path: "/submissions/publish",
+ label: "nav.publish",
+ },
{
name: "submissions.detail",
path: "/submissions/[slug]",
From 48b3494ab047b0e88302db3b68582163c89c06d7 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 3 Mar 2026 12:44:37 -0800
Subject: [PATCH 12/95] [E] Handle collection param on new submission route
---
.../newSubmissionCollectionQuery.graphql.ts | 97 +++++++++++++++++++
.../SubmissionCreateForm.tsx | 12 ++-
.../CollectionTypeahead.tsx | 15 ++-
packages/admin/pages/my-submissions/index.tsx | 8 +-
packages/admin/pages/my-submissions/new.tsx | 66 ++++++++++++-
5 files changed, 189 insertions(+), 9 deletions(-)
create mode 100644 packages/admin/__generated__/newSubmissionCollectionQuery.graphql.ts
diff --git a/packages/admin/__generated__/newSubmissionCollectionQuery.graphql.ts b/packages/admin/__generated__/newSubmissionCollectionQuery.graphql.ts
new file mode 100644
index 000000000..8e7cc7bb0
--- /dev/null
+++ b/packages/admin/__generated__/newSubmissionCollectionQuery.graphql.ts
@@ -0,0 +1,97 @@
+/**
+ * @generated SignedSource<>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+export type newSubmissionCollectionQuery$variables = {
+ slug: string;
+};
+export type newSubmissionCollectionQuery$data = {
+ readonly collection: {
+ readonly id: string;
+ readonly title: string;
+ } | null | undefined;
+};
+export type newSubmissionCollectionQuery = {
+ response: newSubmissionCollectionQuery$data;
+ variables: newSubmissionCollectionQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = [
+ {
+ "defaultValue": null,
+ "kind": "LocalArgument",
+ "name": "slug"
+ }
+],
+v1 = [
+ {
+ "alias": null,
+ "args": [
+ {
+ "kind": "Variable",
+ "name": "slug",
+ "variableName": "slug"
+ }
+ ],
+ "concreteType": "Collection",
+ "kind": "LinkedField",
+ "name": "collection",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "title",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+];
+return {
+ "fragment": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "newSubmissionCollectionQuery",
+ "selections": (v1/*: any*/),
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": (v0/*: any*/),
+ "kind": "Operation",
+ "name": "newSubmissionCollectionQuery",
+ "selections": (v1/*: any*/)
+ },
+ "params": {
+ "cacheID": "22a44e88d8fc9d6b9610cb3194e3eec2",
+ "id": null,
+ "metadata": {},
+ "name": "newSubmissionCollectionQuery",
+ "operationKind": "query",
+ "text": "query newSubmissionCollectionQuery(\n $slug: Slug!\n) {\n collection(slug: $slug) {\n id\n title\n }\n}\n"
+ }
+};
+})();
+
+(node as any).hash = "16a5e64762efa7d4be6fa4af7559f93b";
+
+export default node;
diff --git a/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx b/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx
index 0a879a956..fad7af557 100644
--- a/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx
+++ b/packages/admin/components/composed/submission/SubmissionCreateForm/SubmissionCreateForm.tsx
@@ -27,9 +27,13 @@ type Fields = Omit & {
type Props = {
data: SchemaSelectFragment$key;
+ initialCollection?: { id: string; title: string };
};
-export default function SubmissionCreateForm({ data }: Props) {
+export default function SubmissionCreateForm({
+ data,
+ initialCollection,
+}: Props) {
const { t } = useTranslation();
const router = useRouter();
@@ -66,6 +70,7 @@ export default function SubmissionCreateForm({ data }: Props) {
visibility: "VISIBLE" as Fields["visibility"],
contributors: [] as Fields["contributors"],
files: [] as Fields["files"],
+ ...(initialCollection ? { parentId: initialCollection.id } : {}),
};
const renderForm = useRenderForm(
@@ -87,6 +92,11 @@ export default function SubmissionCreateForm({ data }: Props) {
name="parentId"
label="forms.fields.collection"
required
+ initialOption={
+ initialCollection
+ ? { label: initialCollection.title, value: initialCollection.id }
+ : undefined
+ }
/>
;
const CollectionTypeahead = (
- { control, name, label, disabled, required }: Props,
+ { control, name, label, disabled, required, initialOption }: Props,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ref: Ref,
) => {
@@ -30,6 +30,14 @@ const CollectionTypeahead = (
value: node.entity.id ?? "",
};
});
+
+ if (
+ initialOption &&
+ !options.some((o) => o.value === initialOption.value)
+ ) {
+ return [initialOption, ...options];
+ }
+
return options;
};
@@ -75,7 +83,9 @@ const CollectionTypeahead = (
render={({ field }) => (
extends Omit {
control: Control;
name: Path;
+ initialOption?: { label: string; value: string };
}
export default CollectionTypeahead;
diff --git a/packages/admin/pages/my-submissions/index.tsx b/packages/admin/pages/my-submissions/index.tsx
index 2ae170efc..c70f46a9a 100644
--- a/packages/admin/pages/my-submissions/index.tsx
+++ b/packages/admin/pages/my-submissions/index.tsx
@@ -18,7 +18,11 @@ export default function MySubmissions() {
loadingFallback={}
>
{({ queryRef }) =>
- queryRef ? :
+ queryRef ? (
+
+ ) : (
+
+ )
}
>
@@ -30,7 +34,7 @@ const ListQuery = ({ queryRef }: { queryRef: PreloadedQuery }) => {
viewer: { items },
} = usePreloadedQuery(query, queryRef);
- return ;
+ return ;
};
const query = graphql`
diff --git a/packages/admin/pages/my-submissions/new.tsx b/packages/admin/pages/my-submissions/new.tsx
index 5383837b3..66e0fdce9 100644
--- a/packages/admin/pages/my-submissions/new.tsx
+++ b/packages/admin/pages/my-submissions/new.tsx
@@ -1,14 +1,24 @@
-import { usePreloadedQuery, graphql, PreloadedQuery } from "react-relay";
+import {
+ usePreloadedQuery,
+ useLazyLoadQuery,
+ graphql,
+ PreloadedQuery,
+} from "react-relay";
+import { useRouter } from "next/router";
import { useTranslation } from "react-i18next";
import { QueryTransitionWrapper } from "@wdp/lib/api/components";
+import routeQueryArrayToString from "@wdp/lib/routes/helpers/routeQueryArrayToString";
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";
+import type { newSubmissionCollectionQuery as CollectionQuery } from "@/relay/newSubmissionCollectionQuery.graphql";
export default function NewSubmission() {
const { t } = useTranslation();
+ const router = useRouter();
+ const collectionSlug = routeQueryArrayToString(router.query.collection);
return (
<>
@@ -21,17 +31,56 @@ export default function NewSubmission() {
loadingFallback={}
>
{({ queryRef }) =>
- queryRef ? : null
+ queryRef ? (
+ collectionSlug ? (
+
+ ) : (
+
+ )
+ ) : null
}
>
);
}
-const FormWithData = ({ queryRef }: { queryRef: PreloadedQuery }) => {
+const FormWithData = ({
+ queryRef,
+ initialCollection,
+}: {
+ queryRef: PreloadedQuery;
+ initialCollection?: { id: string; title: string };
+}) => {
const data = usePreloadedQuery(query, queryRef);
- return ;
+ return (
+
+ );
+};
+
+const FormWithCollection = ({
+ queryRef,
+ collectionSlug,
+}: {
+ queryRef: PreloadedQuery;
+ collectionSlug: string;
+}) => {
+ const { collection } = useLazyLoadQuery(
+ collectionQuery,
+ { slug: collectionSlug },
+ { fetchPolicy: "store-or-network" },
+ );
+
+ const initialCollection = collection
+ ? { id: collection.id, title: collection.title }
+ : undefined;
+
+ return (
+
+ );
};
const query = graphql`
@@ -39,3 +88,12 @@ const query = graphql`
...SchemaSelectFragment
}
`;
+
+const collectionQuery = graphql`
+ query newSubmissionCollectionQuery($slug: Slug!) {
+ collection(slug: $slug) {
+ id
+ title
+ }
+ }
+`;
From 28638ee74a3e0cc8d37d8161131f87be9969e326 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 3 Mar 2026 15:16:34 -0800
Subject: [PATCH 13/95] [E] Add review next/prev nav to bottom
---
.../ButtonControl/ButtonControl.styles.ts | 13 +++--
.../buttons/ButtonControl/ButtonControl.tsx | 23 +++++----
.../patterns/ButtonControlRoute.tsx | 15 +++++-
.../ReviewNav/ReviewNav.styles.ts | 14 ++++++
.../SubmissionLayout/ReviewNav/ReviewNav.tsx | 47 +++++++++++++++++++
.../SubmissionLayout/ReviewNav/index.ts | 1 +
.../SubmissionLayout.styles.ts | 18 -------
.../SubmissionLayout/SubmissionLayout.tsx | 25 +++-------
packages/admin/locales/en.json | 1 +
9 files changed, 107 insertions(+), 50 deletions(-)
create mode 100644 packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/ReviewNav.styles.ts
create mode 100644 packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/ReviewNav.tsx
create mode 100644 packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/index.ts
delete mode 100644 packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.styles.ts
diff --git a/packages/admin/components/atomic/buttons/ButtonControl/ButtonControl.styles.ts b/packages/admin/components/atomic/buttons/ButtonControl/ButtonControl.styles.ts
index f54e773bb..3c33075f8 100644
--- a/packages/admin/components/atomic/buttons/ButtonControl/ButtonControl.styles.ts
+++ b/packages/admin/components/atomic/buttons/ButtonControl/ButtonControl.styles.ts
@@ -73,6 +73,7 @@ export const ButtonControl = styled.button<{
export const ButtonText = styled.span<{
$size?: BaseProps["size"];
$icon?: BaseProps["icon"];
+ $iconLeft?: boolean;
}>`
display: inline-block;
white-space: nowrap;
@@ -83,9 +84,13 @@ export const ButtonText = styled.span<{
padding-inline-start: ${pxToRem(4)};
`}
- ${({ $icon }) =>
+ ${({ $icon, $iconLeft }) =>
$icon &&
- css`
- padding-inline-end: ${pxToRem(10)};
- `}
+ ($iconLeft
+ ? css`
+ padding-inline-start: ${pxToRem(10)};
+ `
+ : css`
+ padding-inline-end: ${pxToRem(10)};
+ `)}
`;
diff --git a/packages/admin/components/atomic/buttons/ButtonControl/ButtonControl.tsx b/packages/admin/components/atomic/buttons/ButtonControl/ButtonControl.tsx
index 50174e15e..51827bcf7 100644
--- a/packages/admin/components/atomic/buttons/ButtonControl/ButtonControl.tsx
+++ b/packages/admin/components/atomic/buttons/ButtonControl/ButtonControl.tsx
@@ -11,6 +11,7 @@ const ButtonControl = forwardRef(
children,
iconRotate,
iconChecked,
+ iconLeft,
actions,
allowedActions,
$closeDropdown,
@@ -30,6 +31,15 @@ const ButtonControl = forwardRef(
: onClick
: $closeDropdown;
+ const iconEl = icon && (
+
+ );
+
const content = (
<>
+ {iconLeft && iconEl}
{children && (
-
+
{children}
)}
- {icon && (
-
- )}
+ {!iconLeft && iconEl}
>
);
@@ -72,6 +76,7 @@ interface Props extends Omit {
icon?: IconFactoryProps["icon"];
iconRotate?: number;
iconChecked?: boolean;
+ iconLeft?: boolean;
as?: React.ElementType;
onClick?: React.MouseEventHandler;
"aria-label"?: string;
diff --git a/packages/admin/components/atomic/buttons/ButtonControl/patterns/ButtonControlRoute.tsx b/packages/admin/components/atomic/buttons/ButtonControl/patterns/ButtonControlRoute.tsx
index d2d0a0367..aa6a58da2 100644
--- a/packages/admin/components/atomic/buttons/ButtonControl/patterns/ButtonControlRoute.tsx
+++ b/packages/admin/components/atomic/buttons/ButtonControl/patterns/ButtonControlRoute.tsx
@@ -7,13 +7,26 @@ type NamedLinkProps = React.ComponentProps;
* A button control that opens a drawer
*/
const ButtonControlRoute = forwardRef(
- ({ icon, children, iconChecked, className, ...linkProps }: Props, ref) => {
+ (
+ {
+ icon,
+ children,
+ iconChecked,
+ iconLeft,
+ iconRotate,
+ className,
+ ...linkProps
+ }: Props,
+ ref,
+ ) => {
return (
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/ReviewNav.styles.ts b/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/ReviewNav.styles.ts
new file mode 100644
index 000000000..a670a7cfa
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/ReviewNav.styles.ts
@@ -0,0 +1,14 @@
+import styled from "styled-components";
+import { pxToRem } from "theme/mixins/functions";
+
+export const ReviewNav = styled.nav`
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ border-block-start: 1px solid var(--border-color);
+ padding-block-start: ${pxToRem(24)};
+
+ > a {
+ min-width: 170px;
+ }
+`;
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/ReviewNav.tsx b/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/ReviewNav.tsx
new file mode 100644
index 000000000..5e020de60
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/ReviewNav.tsx
@@ -0,0 +1,47 @@
+import { useTranslation } from "react-i18next";
+import { ButtonControl, ButtonControlRoute } from "components/atomic";
+import * as Styled from "./ReviewNav.styles";
+
+export default function ReviewNav({
+ prevSlug,
+ nextSlug,
+}: {
+ prevSlug: string | null;
+ nextSlug: string | null;
+}) {
+ const { t } = useTranslation();
+
+ return (
+
+ {prevSlug ? (
+
+ {t("actions.review_previous")}
+
+ ) : (
+
+ {t("actions.review_previous")}
+
+ )}
+ {nextSlug ? (
+
+ {t("actions.review_next")}
+
+ ) : (
+
+ {t("actions.review_next")}
+
+ )}
+
+ );
+}
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/index.ts b/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/index.ts
new file mode 100644
index 000000000..17969bb94
--- /dev/null
+++ b/packages/admin/components/composed/submission/SubmissionLayout/ReviewNav/index.ts
@@ -0,0 +1 @@
+export { default } from "./ReviewNav";
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.styles.ts b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.styles.ts
deleted file mode 100644
index 0d4a8fcd4..000000000
--- a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.styles.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import styled from "styled-components";
-
-export const NavRow = styled.nav`
- display: flex;
- justify-content: space-between;
- align-items: baseline;
-`;
-
-export const ReviewNextLink = styled.a`
- display: flex;
- align-items: center;
- font-size: var(--font-size-sm);
- white-space: nowrap;
-
- > * + * {
- margin-inline-start: 5px;
- }
-`;
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
index aa16ca94a..9dc94f545 100644
--- a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
+++ b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
@@ -7,16 +7,14 @@ import {
ButtonControlConfirm,
ButtonControlDrawer,
ButtonControlRoute,
- NamedLink,
ReviewFeedback,
StatusBadge,
} from "components/atomic";
-import { IconFactory } from "components/factories";
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";
-import * as Styled from "./SubmissionLayout.styles";
+import ReviewNav from "./ReviewNav";
export default function SubmissionLayout({
children,
@@ -49,6 +47,8 @@ export default function SubmissionLayout({
// need to fetch the next page of results at the boundary (e.g. after the last
// submission on a page, fetch the next page). For now we use the mock array.
const currentIndex = MOCK_SUBMISSIONS.findIndex((s) => s.slug === slug);
+ const prevSlug =
+ currentIndex > 0 ? MOCK_SUBMISSIONS[currentIndex - 1].slug : null;
const nextSlug =
currentIndex >= 0 && currentIndex < MOCK_SUBMISSIONS.length - 1
? MOCK_SUBMISSIONS[currentIndex + 1].slug
@@ -117,21 +117,7 @@ export default function SubmissionLayout({
<>
-
-
- {nextSlug && parentRoute === "submissions" && (
-
-
- {t("actions.review_next")}
-
-
-
- )}
-
+
)}
{children}
+ {parentRoute === "submissions" && !isEditing && (
+
+ )}
>
);
diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json
index cbe51b632..75803a815 100644
--- a/packages/admin/locales/en.json
+++ b/packages/admin/locales/en.json
@@ -426,6 +426,7 @@
},
"review_submission": "Review",
"review_next": "Review Next",
+ "review_previous": "Review Previous",
"submit_for_review": "Submit",
"submit_for_review_confirm": "Are you sure you're ready to submit this for review? It will be sent to a reviewer for evaluation.",
"resubmit_for_review": "Resubmit",
From 875239c70bfd6f81d299e506544bb25c07252bc9 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Tue, 3 Mar 2026 16:38:50 -0800
Subject: [PATCH 14/95] [F] Scaffold dashboard layout + messaging for
depositors
---
.../MessageBanner/MessageBanner.styles.ts | 70 ++++++++++
.../atomic/MessageBanner/MessageBanner.tsx | 48 +++++++
.../components/atomic/MessageBanner/index.ts | 1 +
.../ReviewFeedback/ReviewFeedback.styles.ts | 17 ---
.../atomic/ReviewFeedback/ReviewFeedback.tsx | 17 ---
.../components/atomic/ReviewFeedback/index.ts | 1 -
packages/admin/components/atomic/index.ts | 2 +-
.../components/auth/RouteGuard/RouteGuard.tsx | 10 +-
.../UnauthorizedMessage.tsx | 6 +
.../DashboardLayout/DashboardLayout.styles.ts | 12 ++
.../DashboardLayout/DashboardLayout.tsx | 41 ++++++
.../DashboardSubmitter/DashboardSubmitter.tsx | 120 ++++++++++++++++++
.../dashboard/DashboardSubmitter/index.ts | 1 +
.../components/composed/dashboard/index.ts | 1 +
.../SubmissionLayout/SubmissionLayout.tsx | 5 +-
packages/admin/locales/en.json | 7 +-
16 files changed, 317 insertions(+), 42 deletions(-)
create mode 100644 packages/admin/components/atomic/MessageBanner/MessageBanner.styles.ts
create mode 100644 packages/admin/components/atomic/MessageBanner/MessageBanner.tsx
create mode 100644 packages/admin/components/atomic/MessageBanner/index.ts
delete mode 100644 packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.styles.ts
delete mode 100644 packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.tsx
delete mode 100644 packages/admin/components/atomic/ReviewFeedback/index.ts
create mode 100644 packages/admin/components/composed/dashboard/DashboardSubmitter/DashboardSubmitter.tsx
create mode 100644 packages/admin/components/composed/dashboard/DashboardSubmitter/index.ts
diff --git a/packages/admin/components/atomic/MessageBanner/MessageBanner.styles.ts b/packages/admin/components/atomic/MessageBanner/MessageBanner.styles.ts
new file mode 100644
index 000000000..173c4ccbd
--- /dev/null
+++ b/packages/admin/components/atomic/MessageBanner/MessageBanner.styles.ts
@@ -0,0 +1,70 @@
+import styled, { css } from "styled-components";
+import { pxToRem } from "theme/mixins/functions";
+import { aButton } from "theme/mixins/appearance";
+
+type Variant = "warning" | "info";
+
+const variantColors = {
+ warning: {
+ bg: "var(--yellowtint)",
+ border: "var(--yellowdark)",
+ header: "var(--yellowdark)",
+ },
+ info: {
+ bg: "var(--brand10)",
+ border: "transparent",
+ header: "var(--brand100)",
+ },
+};
+
+export const Wrapper = styled.div<{ $variant: Variant }>`
+ display: flex;
+ gap: ${pxToRem(12)};
+ align-items: flex-start;
+ border-radius: ${pxToRem(10)};
+ padding: ${pxToRem(20)} ${pxToRem(24)};
+ margin-block-end: var(--form-grid-row-gap);
+
+ ${({ $variant }) => {
+ const colors = variantColors[$variant];
+ return css`
+ background-color: ${colors.bg};
+ border: 1px solid ${colors.border};
+ `;
+ }}
+`;
+
+export const Icon = styled.span<{ $variant: Variant }>`
+ flex-shrink: 0;
+ color: ${({ $variant }) => variantColors[$variant].header};
+ --icon-background-color: ${({ $variant }) => variantColors[$variant].header};
+ --icon-foreground-color: ${({ $variant }) => variantColors[$variant].bg};
+`;
+
+export const Content = styled.div`
+ flex: 1;
+`;
+
+export const Header = styled.h3<{ $variant: Variant; $hasMessage: boolean }>`
+ color: ${({ $variant }) => variantColors[$variant].header};
+
+ ${({ $hasMessage }) =>
+ $hasMessage &&
+ css`
+ padding-block-end: ${pxToRem(12)};
+ `}
+`;
+
+export const DismissButton = styled.button`
+ ${aButton(true)}
+ padding: ${pxToRem(5)};
+ height: ${pxToRem(26)};
+ width: ${pxToRem(26)};
+ border-radius: ${pxToRem(4)};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-block-start: -6px;
+`;
+
+export const Message = styled.div``;
diff --git a/packages/admin/components/atomic/MessageBanner/MessageBanner.tsx b/packages/admin/components/atomic/MessageBanner/MessageBanner.tsx
new file mode 100644
index 000000000..889751a14
--- /dev/null
+++ b/packages/admin/components/atomic/MessageBanner/MessageBanner.tsx
@@ -0,0 +1,48 @@
+import { useTranslation } from "react-i18next";
+import { IconFactory } from "components/factories";
+import * as Styled from "./MessageBanner.styles";
+import type { IconKeys } from "components/factories/IconFactory/IconFactory";
+
+const MessageBanner = ({
+ name,
+ message,
+ variant = "warning",
+ icon,
+ onDismiss,
+}: Props) => {
+ const { t } = useTranslation();
+
+ return (
+
+ {icon && (
+
+
+
+ )}
+
+
+ {name}
+
+ {message && {message}}
+
+ {onDismiss && (
+
+
+
+ )}
+
+ );
+};
+
+interface Props {
+ name: string;
+ message?: string | React.JSX.Element;
+ variant?: "warning" | "info";
+ icon?: IconKeys;
+ onDismiss?: () => void;
+}
+
+export default MessageBanner;
diff --git a/packages/admin/components/atomic/MessageBanner/index.ts b/packages/admin/components/atomic/MessageBanner/index.ts
new file mode 100644
index 000000000..1860c4e0f
--- /dev/null
+++ b/packages/admin/components/atomic/MessageBanner/index.ts
@@ -0,0 +1 @@
+export { default as MessageBanner } from "./MessageBanner";
diff --git a/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.styles.ts b/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.styles.ts
deleted file mode 100644
index 2afdf22a5..000000000
--- a/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.styles.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import styled from "styled-components";
-import { pxToRem } from "theme/mixins/functions";
-
-export const Wrapper = styled.div`
- border-radius: ${pxToRem(10)};
- padding: ${pxToRem(20)} ${pxToRem(24)};
- background-color: var(--yellowtint);
- border: 1px solid var(--yellowdark);
- margin-block-end: var(--form-grid-row-gap);
-`;
-
-export const Header = styled.h3`
- padding-block-end: ${pxToRem(12)};
- color: var(--yellowdark);
-`;
-
-export const Message = styled.div``;
diff --git a/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.tsx b/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.tsx
deleted file mode 100644
index 30b7bb2fb..000000000
--- a/packages/admin/components/atomic/ReviewFeedback/ReviewFeedback.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as Styled from "./ReviewFeedback.styles";
-
-const ReviewFeedback = ({ name, message }: Props) => {
- return (
-
- {name}
- {message && {message}}
-
- );
-};
-
-interface Props {
- name: string;
- message?: string | React.JSX.Element;
-}
-
-export default ReviewFeedback;
diff --git a/packages/admin/components/atomic/ReviewFeedback/index.ts b/packages/admin/components/atomic/ReviewFeedback/index.ts
deleted file mode 100644
index bc2d3fd7c..000000000
--- a/packages/admin/components/atomic/ReviewFeedback/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as ReviewFeedback } from "./ReviewFeedback";
diff --git a/packages/admin/components/atomic/index.ts b/packages/admin/components/atomic/index.ts
index c87bd7626..a371e491b 100644
--- a/packages/admin/components/atomic/index.ts
+++ b/packages/admin/components/atomic/index.ts
@@ -13,6 +13,6 @@ export * from "./buttons";
export * from "./links";
export * from "./loading";
export * from "./MessageBlock";
-export * from "./ReviewFeedback";
+export * from "./MessageBanner";
export * from "./StatusBadge";
export * from "./images";
diff --git a/packages/admin/components/auth/RouteGuard/RouteGuard.tsx b/packages/admin/components/auth/RouteGuard/RouteGuard.tsx
index b07bc02c6..5478d7a32 100644
--- a/packages/admin/components/auth/RouteGuard/RouteGuard.tsx
+++ b/packages/admin/components/auth/RouteGuard/RouteGuard.tsx
@@ -1,4 +1,5 @@
import { useEffect } from "react";
+import { useRouter } from "next/router";
import { useSignInOut } from "@wdp/lib/api/hooks/useIsAuthenticated";
import { useIsAuthorized } from "hooks";
import { useViewerContext } from "contexts";
@@ -6,6 +7,7 @@ import UnauthorizedMessage from "../UnauthorizedMessage";
export default function RouteGuard({ children }: Props) {
const { handleSignInOut, isAuthenticated } = useSignInOut();
+ const router = useRouter();
// Check if the viewer context is loading (auth state is undetermined)
const { loading } = useViewerContext();
@@ -19,9 +21,13 @@ export default function RouteGuard({ children }: Props) {
actions: "admin.access",
});
- // If the user is authorized, show children
+ // Allow authenticated users to access the homepage and my-submissions routes
+ const isOpenRoute =
+ router.pathname === "/" || router.pathname.startsWith("/my-submissions");
+
+ // If the user is authorized or on an open route, show children
// If we've finished loading the page and the user is not authorized, show message
- return isAuthorized ? (
+ return isAuthorized || isOpenRoute ? (
<>{children}>
) : loading ? null : (
diff --git a/packages/admin/components/auth/UnauthorizedMessage/UnauthorizedMessage.tsx b/packages/admin/components/auth/UnauthorizedMessage/UnauthorizedMessage.tsx
index fe89683b7..79af3c35d 100644
--- a/packages/admin/components/auth/UnauthorizedMessage/UnauthorizedMessage.tsx
+++ b/packages/admin/components/auth/UnauthorizedMessage/UnauthorizedMessage.tsx
@@ -1,4 +1,5 @@
import React, { useMemo } from "react";
+import Link from "next/link";
import useMaybeFragment from "@wdp/lib/api/hooks/useMaybeFragment";
import { graphql } from "react-relay";
import { MessageBlock } from "components/atomic";
@@ -35,6 +36,11 @@ export default function UnauthorizedMessage({
Please check with your account manager if you need access to this
tool.
+
+
+ If you're looking to submit, go to{" "}
+ your submissions page.
+
);
diff --git a/packages/admin/components/composed/dashboard/DashboardLayout/DashboardLayout.styles.ts b/packages/admin/components/composed/dashboard/DashboardLayout/DashboardLayout.styles.ts
index 3d139691a..70adf79ad 100644
--- a/packages/admin/components/composed/dashboard/DashboardLayout/DashboardLayout.styles.ts
+++ b/packages/admin/components/composed/dashboard/DashboardLayout/DashboardLayout.styles.ts
@@ -2,6 +2,14 @@ import { pxToRem } from "@wdp/lib/theme/functions";
import styled from "styled-components";
import { fluidScale, respond } from "theme/mixins/base";
+export const BannerArea = styled.div`
+ grid-column: 1 / -1;
+
+ > * {
+ margin-block-end: 0;
+ }
+`;
+
export const Wrapper = styled.div`
display: grid;
gap: ${fluidScale("52px", "24px")};
@@ -9,6 +17,10 @@ export const Wrapper = styled.div`
grid-template-rows: repeat(2, auto);
padding-block-start: ${pxToRem(80)};
+ &:has(${BannerArea}) {
+ padding-block-start: ${pxToRem(40)};
+ }
+
${respond(`grid-template-columns: 1fr;`, 40)}
> *:first-child {
diff --git a/packages/admin/components/composed/dashboard/DashboardLayout/DashboardLayout.tsx b/packages/admin/components/composed/dashboard/DashboardLayout/DashboardLayout.tsx
index 3bd6cdcd4..b2539a398 100644
--- a/packages/admin/components/composed/dashboard/DashboardLayout/DashboardLayout.tsx
+++ b/packages/admin/components/composed/dashboard/DashboardLayout/DashboardLayout.tsx
@@ -1,16 +1,57 @@
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useKeycloak } from "@react-keycloak/ssr";
import { graphql, usePreloadedQuery, PreloadedQuery } from "react-relay";
import { useViewerContext } from "contexts";
+import { useIsAuthorized } from "hooks";
+import { MessageBanner } from "components/atomic";
import { DashboardLayoutQuery } from "@/relay/DashboardLayoutQuery.graphql";
import DashboardInstallation from "../DashboardInstallation";
import DashboardCollections from "../DashboardCollections";
import DashboardItems from "../DashboardItems";
+import DashboardSubmitter from "../DashboardSubmitter";
import * as Styled from "./DashboardLayout.styles";
+import type { KeycloakInstance } from "keycloak-js";
export default function DashboardLayout({ queryRef }: Props) {
const queryData = usePreloadedQuery(query, queryRef);
+ const { t } = useTranslation();
const { globalAdmin } = useViewerContext();
+ const { keycloak } = useKeycloak();
+ const userId = keycloak?.subject;
+
+ const isAdmin = useIsAuthorized({ actions: "admin.access" });
+ const cookieName = `no_admin_banner_dismissed_${userId}`;
+ const [bannerDismissed, setBannerDismissed] = useState(() =>
+ typeof document !== "undefined"
+ ? document.cookie.includes(`${cookieName}=1`)
+ : false,
+ );
+
+ if (!isAdmin) {
+ return (
+
+ {!bannerDismissed && (
+
+ {
+ document.cookie = `${cookieName}=1; path=/; max-age=31536000`;
+ setBannerDismissed(true);
+ }}
+ />
+
+ )}
+
+
+ );
+ }
+
return (
diff --git a/packages/admin/components/composed/dashboard/DashboardSubmitter/DashboardSubmitter.tsx b/packages/admin/components/composed/dashboard/DashboardSubmitter/DashboardSubmitter.tsx
new file mode 100644
index 000000000..dee7a1206
--- /dev/null
+++ b/packages/admin/components/composed/dashboard/DashboardSubmitter/DashboardSubmitter.tsx
@@ -0,0 +1,120 @@
+import { useTranslation } from "react-i18next";
+import CoverPlaceholder from "@wdp/lib/atomic/CoverPlaceholder";
+import { ContentHeader, PageCountActions } from "components/layout";
+import ModelList from "components/composed/model/ModelList";
+import ModelColumns from "components/composed/model/ModelColumns";
+import { ViewOptions } from "utils/view-options";
+import {
+ MOCK_SUBMISSIONS,
+ type SubmissionNode,
+} from "components/composed/submission/SubmissionList/mockData";
+import type { ColumnDef } from "@tanstack/react-table";
+
+type MockConnection = { nodes: readonly SubmissionNode[] };
+
+const publishedSubmissions = MOCK_SUBMISSIONS.filter(
+ (s) => s.status === "Published",
+);
+
+function ThumbnailColumn(): ColumnDef {
+ return {
+ header: () => Thumbnail,
+ id: "thumbnail",
+ accessorFn: (row) => row,
+ enableSorting: false,
+ cell: (info) => {
+ const row = info.getValue();
+ return (
+
+ );
+ },
+ meta: { cellType: "thumbnail" },
+ };
+}
+
+export default function DashboardSubmitter() {
+ const { t } = useTranslation();
+
+ const allData: MockConnection = { nodes: MOCK_SUBMISSIONS };
+
+ const publishedData: MockConnection = {
+ nodes: publishedSubmissions,
+ };
+
+ const submissionColumns = [
+ ThumbnailColumn(),
+ ModelColumns.NameColumn({
+ accessor: "title",
+ route: "my-submissions.detail",
+ enableSorting: false,
+ }),
+ ModelColumns.StringColumn({
+ id: "status",
+ header: () => t("lists.status_column"),
+ }),
+ ModelColumns.UpdatedAtColumn({
+ enableSorting: false,
+ }),
+ ];
+
+ const publicationColumns = [
+ ThumbnailColumn(),
+ ModelColumns.NameColumn({
+ accessor: "title",
+ route: "my-submissions.detail",
+ enableSorting: false,
+ }),
+ ModelColumns.StringColumn({
+ id: "collection",
+ header: () => t("lists.collection_column"),
+ }),
+ ModelColumns.UpdatedAtColumn({
+ enableSorting: false,
+ }),
+ ];
+
+ return (
+ <>
+
+
+
+
+ view={ViewOptions.grid}
+ columns={submissionColumns}
+ data={allData}
+ modelName="submission"
+ disableSortBy
+ />
+
+
+
+
+
+ view={ViewOptions.grid}
+ columns={publicationColumns}
+ data={publishedData}
+ modelName="submission"
+ disableSortBy
+ />
+
+ >
+ );
+}
diff --git a/packages/admin/components/composed/dashboard/DashboardSubmitter/index.ts b/packages/admin/components/composed/dashboard/DashboardSubmitter/index.ts
new file mode 100644
index 000000000..81aa5015f
--- /dev/null
+++ b/packages/admin/components/composed/dashboard/DashboardSubmitter/index.ts
@@ -0,0 +1 @@
+export { default } from "./DashboardSubmitter";
diff --git a/packages/admin/components/composed/dashboard/index.ts b/packages/admin/components/composed/dashboard/index.ts
index e2e729d4f..636535031 100644
--- a/packages/admin/components/composed/dashboard/index.ts
+++ b/packages/admin/components/composed/dashboard/index.ts
@@ -1 +1,2 @@
export { default as DashboardLayout } from "./DashboardLayout";
+export { default as DashboardSubmitter } from "./DashboardSubmitter";
diff --git a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
index 9dc94f545..ceb81d40d 100644
--- a/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
+++ b/packages/admin/components/composed/submission/SubmissionLayout/SubmissionLayout.tsx
@@ -7,7 +7,7 @@ import {
ButtonControlConfirm,
ButtonControlDrawer,
ButtonControlRoute,
- ReviewFeedback,
+ MessageBanner,
StatusBadge,
} from "components/atomic";
import { PageHeader, BackToAll } from "components/layout";
@@ -130,7 +130,8 @@ export default function SubmissionLayout({
buttons={buttons}
/>
{submission?.reviewComment && (
-
diff --git a/packages/admin/locales/en.json b/packages/admin/locales/en.json
index 75803a815..cad14eadc 100644
--- a/packages/admin/locales/en.json
+++ b/packages/admin/locales/en.json
@@ -204,7 +204,8 @@
"review": "Review",
"publish": "Publish",
"new_submission": "New Submission",
- "history": "History"
+ "history": "History",
+ "my_publications": "My Publications"
},
"messages": {
"marked_for_purge": "Marked for purge",
@@ -699,7 +700,9 @@
},
"dashboard": {
"installation_header": "Installation",
- "current_version_label": "Current Version"
+ "current_version_label": "Current Version",
+ "no_admin_access_title": "No Admin Access",
+ "no_admin_access_message": "This account has no admin access. If you're here to submit, you're in the right place and can ignore this message. If you expected to have admin access, please contact your site administrator."
},
"search": {
"results": "Search results",
From b4f69ff84873c1075d4306142433437fe14b6922 Mon Sep 17 00:00:00 2001
From: 1aurend
Date: Mon, 9 Mar 2026 09:58:39 -0700
Subject: [PATCH 15/95] [C] Update schema
---
packages/admin/__schema__/schema.json | 332 +++++++++++++++++++++++
packages/admin/schema.graphql | 71 +++++
packages/admin/types/graphql-schema.d.ts | 109 +++++++-
3 files changed, 508 insertions(+), 4 deletions(-)
diff --git a/packages/admin/__schema__/schema.json b/packages/admin/__schema__/schema.json
index fdbb23e51..7bcd8a446 100644
--- a/packages/admin/__schema__/schema.json
+++ b/packages/admin/__schema__/schema.json
@@ -10001,6 +10001,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "canPreview",
+ "description": "Whether the current user has permission to preview this entity.\n",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AuthorizationResult",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "canPurge",
"description": "Whether the current user has permission to purge this entity.\n",
@@ -12834,6 +12850,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "canPreview",
+ "description": "Whether the current user has permission to preview this entity.\n",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AuthorizationResult",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "canPurge",
"description": "Whether the current user has permission to purge this entity.\n",
@@ -19129,6 +19161,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "canPreview",
+ "description": "Whether the current user has permission to preview this entity.\n",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AuthorizationResult",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "canPurge",
"description": "Whether the current user has permission to purge this entity.\n",
@@ -39054,6 +39102,11 @@
"name": "SubmissionCommentDestroyPayload",
"ofType": null
},
+ {
+ "kind": "OBJECT",
+ "name": "SubmissionDestroyPayload",
+ "ofType": null
+ },
{
"kind": "OBJECT",
"name": "SubmissionTargetReviewerDestroyPayload",
@@ -41531,6 +41584,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "canPreview",
+ "description": "Whether the current user has permission to preview this entity.\n",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AuthorizationResult",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "canPurge",
"description": "Whether the current user has permission to purge this entity.\n",
@@ -44399,6 +44468,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "canPreview",
+ "description": "Whether the current user has permission to preview this entity.\n",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AuthorizationResult",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "canPurge",
"description": "Whether the current user has permission to purge this entity.\n",
@@ -62905,6 +62990,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "canPreview",
+ "description": "Whether the current user has permission to preview this entity.\n",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AuthorizationResult",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "canPurge",
"description": "Whether the current user has permission to purge this entity.\n",
@@ -74674,6 +74775,35 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "submissionDestroy",
+ "description": "Destroy a single `Submission` record.\n",
+ "args": [
+ {
+ "name": "input",
+ "description": "Parameters for SubmissionDestroy",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "SubmissionDestroyInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "SubmissionDestroyPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "submissionLeaveReview",
"description": "Leave a review on a submission.\n\nThis effectively acts as an upsert and can be run by itself,\nor as a response to `submissionRequestReview`.\n",
@@ -101365,6 +101495,11 @@
"name": "SubmissionCreatePayload",
"ofType": null
},
+ {
+ "kind": "OBJECT",
+ "name": "SubmissionDestroyPayload",
+ "ofType": null
+ },
{
"kind": "OBJECT",
"name": "SubmissionLeaveReviewPayload",
@@ -104983,6 +105118,187 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "SubmissionDestroyInput",
+ "description": "Autogenerated input type of SubmissionDestroy",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "submissionId",
+ "description": "The submission to destroy.\n",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "SubmissionDestroyPayload",
+ "description": "Autogenerated return type of SubmissionDestroy.",
+ "fields": [
+ {
+ "name": "attributeErrors",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "MutationAttributeError",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyed",
+ "description": "Whether or not the model was successfully destroyed. If false, check globalErrors",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyedId",
+ "description": "The ID of the deleted model",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "UserError",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": true,
+ "deprecationReason": "Use attributeErrors or globalErrors"
+ },
+ {
+ "name": "globalErrors",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "MutationGlobalError",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "haltCode",
+ "description": "Not presently used",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "DestroyMutationPayload",
+ "ofType": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "StandardMutationPayload",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "SubmissionEdge",
@@ -120244,6 +120560,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "canPreview",
+ "description": "Whether this user has the ability to preview content in the system.\n",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AuthorizationResult",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "canReceiveReviewRequests",
"description": "Whether this user is a reviewer on **any** submission targets, and should see information about\npotential review requests in the UI.\n\nThis is meant to be checked on the `Query.viewer`.\n",
diff --git a/packages/admin/schema.graphql b/packages/admin/schema.graphql
index cc924d8df..235c3e6ae 100644
--- a/packages/admin/schema.graphql
+++ b/packages/admin/schema.graphql
@@ -1990,6 +1990,11 @@ interface ChildEntity implements Accessible & CommonPermissions & Entity & Entit
"""
canManageAccess: AuthorizationResult!
"""
+ Whether the current user has permission to preview this entity.
+
+ """
+ canPreview: AuthorizationResult!
+ """
Whether the current user has permission to purge this entity.
"""
@@ -2625,6 +2630,11 @@ type Collection implements Accessible & Attachable & Attributable & ChildEntity
"""
canManageAccess: AuthorizationResult!
"""
+ Whether the current user has permission to preview this entity.
+
+ """
+ canPreview: AuthorizationResult!
+ """
Whether the current user has permission to purge this entity.
"""
@@ -3695,6 +3705,11 @@ type Community implements Accessible & Attachable & CommonModel & CommonPermissi
"""
canManageAccess: AuthorizationResult!
"""
+ Whether the current user has permission to preview this entity.
+
+ """
+ canPreview: AuthorizationResult!
+ """
Whether the current user has permission to purge this entity.
"""
@@ -8870,6 +8885,11 @@ interface Entity implements Accessible & CommonPermissions & EntityBase & Entity
"""
canManageAccess: AuthorizationResult!
"""
+ Whether the current user has permission to preview this entity.
+
+ """
+ canPreview: AuthorizationResult!
+ """
Whether the current user has permission to purge this entity.
"""
@@ -9533,6 +9553,11 @@ interface EntityPermissions implements Submittable {
"""
canManageAccess: AuthorizationResult!
"""
+ Whether the current user has permission to preview this entity.
+
+ """
+ canPreview: AuthorizationResult!
+ """
Whether the current user has permission to purge this entity.
"""
@@ -14333,6 +14358,11 @@ type Item implements Accessible & Attachable & Attributable & ChildEntity & Comm
"""
canManageAccess: AuthorizationResult!
"""
+ Whether the current user has permission to preview this entity.
+
+ """
+ canPreview: AuthorizationResult!
+ """
Whether the current user has permission to purge this entity.
"""
@@ -17059,6 +17089,14 @@ type Mutation {
input: SubmissionCreateInput!
): SubmissionCreatePayload
"""
+ Destroy a single `Submission` record.
+
+ """
+ submissionDestroy(
+ """Parameters for SubmissionDestroy"""
+ input: SubmissionDestroyInput!
+ ): SubmissionDestroyPayload
+ """
Leave a review on a submission.
This effectively acts as an upsert and can be run by itself,
@@ -23816,6 +23854,34 @@ type SubmissionDepositTarget implements CommonModel & CommonPermissions & HasDef
updatedOn: ISO8601Date!
}
+"""Autogenerated input type of SubmissionDestroy"""
+input SubmissionDestroyInput {
+ """A unique identifier for the client performing the mutation."""
+ clientMutationId: String
+ """
+ The submission to destroy.
+
+ """
+ submissionId: ID!
+}
+
+"""Autogenerated return type of SubmissionDestroy."""
+type SubmissionDestroyPayload implements DestroyMutationPayload & StandardMutationPayload {
+ attributeErrors: [MutationAttributeError!]!
+ """A unique identifier for the client performing the mutation."""
+ clientMutationId: String
+ """
+ Whether or not the model was successfully destroyed. If false, check globalErrors
+ """
+ destroyed: Boolean
+ """The ID of the deleted model"""
+ destroyedId: ID
+ errors: [UserError!]! @deprecated(reason: "Use attributeErrors or globalErrors")
+ globalErrors: [MutationGlobalError!]!
+ """Not presently used"""
+ haltCode: String
+}
+
"""
An edge in a connection for `Submission`.
@@ -27706,6 +27772,11 @@ type User implements AccessGrantSubject & CommonModel & CommonPermissions & Expo
"""
canDestroy: AuthorizationResult!
"""
+ Whether this user has the ability to preview content in the system.
+
+ """
+ canPreview: AuthorizationResult!
+ """
Whether this user is a reviewer on **any** submission targets, and should see information about
potential review requests in the UI.
diff --git a/packages/admin/types/graphql-schema.d.ts b/packages/admin/types/graphql-schema.d.ts
index 7ef0e37e4..5a3d71394 100644
--- a/packages/admin/types/graphql-schema.d.ts
+++ b/packages/admin/types/graphql-schema.d.ts
@@ -1852,6 +1852,11 @@ export type ChildEntity = {
*
*/
canManageAccess: AuthorizationResult;
+ /**
+ * Whether the current user has permission to preview this entity.
+ *
+ */
+ canPreview: AuthorizationResult;
/**
* Whether the current user has permission to purge this entity.
*
@@ -2471,6 +2476,11 @@ export type Collection = Accessible & Attachable & Attributable & ChildEntity &
*
*/
canManageAccess: AuthorizationResult;
+ /**
+ * Whether the current user has permission to preview this entity.
+ *
+ */
+ canPreview: AuthorizationResult;
/**
* Whether the current user has permission to purge this entity.
*
@@ -3414,6 +3424,11 @@ export type Community = Accessible & Attachable & CommonModel & CommonPermission
*
*/
canManageAccess: AuthorizationResult;
+ /**
+ * Whether the current user has permission to preview this entity.
+ *
+ */
+ canPreview: AuthorizationResult;
/**
* Whether the current user has permission to purge this entity.
*
@@ -8613,6 +8628,11 @@ export type Entity = {
*
*/
canManageAccess: AuthorizationResult;
+ /**
+ * Whether the current user has permission to preview this entity.
+ *
+ */
+ canPreview: AuthorizationResult;
/**
* Whether the current user has permission to purge this entity.
*
@@ -9334,6 +9354,11 @@ export type EntityPermissions = {
*
*/
canManageAccess: AuthorizationResult;
+ /**
+ * Whether the current user has permission to preview this entity.
+ *
+ */
+ canPreview: AuthorizationResult;
/**
* Whether the current user has permission to purge this entity.
*
@@ -13949,6 +13974,11 @@ export type Item = Accessible & Attachable & Attributable & ChildEntity & Common
*
*/
canManageAccess: AuthorizationResult;
+ /**
+ * Whether the current user has permission to preview this entity.
+ *
+ */
+ canPreview: AuthorizationResult;
/**
* Whether the current user has permission to purge this entity.
*
@@ -16523,6 +16553,11 @@ export type Mutation = {
*
*/
submissionCreate?: Maybe;
+ /**
+ * Destroy a single `Submission` record.
+ *
+ */
+ submissionDestroy?: Maybe;
/**
* Leave a review on a submission.
*
@@ -17236,6 +17271,15 @@ export type MutationSubmissionCreateArgs = {
};
+/**
+ * The entry point for making changes to the data within the Meru API.
+ *
+ */
+export type MutationSubmissionDestroyArgs = {
+ input: SubmissionDestroyInput;
+};
+
+
/**
* The entry point for making changes to the data within the Meru API.
*
@@ -23941,6 +23985,34 @@ export type SubmissionDepositTarget = CommonModel & CommonPermissions & HasDefau
updatedOn: Scalars['ISO8601Date']['output'];
};
+/** Autogenerated input type of SubmissionDestroy */
+export type SubmissionDestroyInput = {
+ /** A unique identifier for the client performing the mutation. */
+ clientMutationId?: InputMaybe;
+ /**
+ * The submission to destroy.
+ *
+ */
+ submissionId: Scalars['ID']['input'];
+};
+
+/** Autogenerated return type of SubmissionDestroy. */
+export type SubmissionDestroyPayload = DestroyMutationPayload & StandardMutationPayload & {
+ __typename?: 'SubmissionDestroyPayload';
+ attributeErrors: Array;
+ /** A unique identifier for the client performing the mutation. */
+ clientMutationId?: Maybe;
+ /** Whether or not the model was successfully destroyed. If false, check globalErrors */
+ destroyed?: Maybe;
+ /** The ID of the deleted model */
+ destroyedId?: Maybe;
+ /** @deprecated Use attributeErrors or globalErrors */
+ errors: Array;
+ globalErrors: Array;
+ /** Not presently used */
+ haltCode?: Maybe;
+};
+
/**
* An edge in a connection for `Submission`.
*
@@ -27828,6 +27900,11 @@ export type User = AccessGrantSubject & CommonModel & CommonPermissions & Expose
*
*/
canDestroy: AuthorizationResult;
+ /**
+ * Whether this user has the ability to preview content in the system.
+ *
+ */
+ canPreview: AuthorizationResult;
/**
* Whether this user is a reviewer on **any** submission targets, and should see information about
* potential review requests in the UI.
@@ -28954,7 +29031,7 @@ export type ResolversInterfaceTypes> = {
ContributorAttribution: ( ContributorCollectionAttribution ) | ( ContributorItemAttribution );
ContributorBase: ( OrganizationContributor ) | ( PersonContributor );
DescribesSchema: ( HierarchicalSchemaRank ) | ( HierarchicalSchemaVersionRank ) | ( SchemaDefinition ) | ( SchemaVersion );
- DestroyMutationPayload: ( ContributorUserLinkDestroyPayload ) | ( ControlledVocabularyDestroyPayload ) | ( DestroyAnnouncementPayload ) | ( DestroyAssetPayload ) | ( DestroyCollectionPayload ) | ( DestroyCommunityPayload ) | ( DestroyContributionPayload ) | ( DestroyContributorPayload ) | ( DestroyEntityLinkPayload ) | ( DestroyItemPayload ) | ( DestroyOrderingPayload ) | ( DestroyPagePayload ) | ( EntityPurgePayload ) | ( HarvestMappingDestroyPayload ) | ( HarvestMetadataMappingDestroyPayload ) | ( HarvestSourceDestroyPayload ) | ( PermalinkDestroyPayload ) | ( SubmissionCommentDestroyPayload ) | ( SubmissionTargetReviewerDestroyPayload );
+ DestroyMutationPayload: ( ContributorUserLinkDestroyPayload ) | ( ControlledVocabularyDestroyPayload ) | ( DestroyAnnouncementPayload ) | ( DestroyAssetPayload ) | ( DestroyCollectionPayload ) | ( DestroyCommunityPayload ) | ( DestroyContributionPayload ) | ( DestroyContributorPayload ) | ( DestroyEntityLinkPayload ) | ( DestroyItemPayload ) | ( DestroyOrderingPayload ) | ( DestroyPagePayload ) | ( EntityPurgePayload ) | ( HarvestMappingDestroyPayload ) | ( HarvestMetadataMappingDestroyPayload ) | ( HarvestSourceDestroyPayload ) | ( PermalinkDestroyPayload ) | ( SubmissionCommentDestroyPayload ) | ( SubmissionDestroyPayload ) | ( SubmissionTargetReviewerDestroyPayload );
Entity: ( Omit & { parent?: Maybe } ) | ( Community ) | ( Omit- & { parent?: Maybe } );
EntityBase: ( Omit & { parent?: Maybe } ) | ( Community ) | ( Omit
- & { parent?: Maybe } );
EntityContextualPermissions: ( Omit & { parent?: Maybe } ) | ( Community ) | ( Omit
- & { parent?: Maybe } );
@@ -29017,7 +29094,7 @@ export type ResolversInterfaceTypes> = {
Searchable: ( Omit & { parent?: Maybe } ) | ( Community ) | ( Omit
- & { parent?: Maybe } ) | ( Ordering ) | ( Query ) | ( SchemaVersion );
SearchableProperty: ( BooleanProperty ) | ( DateProperty ) | ( FloatProperty ) | ( FullTextProperty ) | ( IntegerProperty ) | ( MarkdownProperty ) | ( MultiselectProperty ) | ( SearchableCoreProperty ) | ( SelectProperty ) | ( StringProperty ) | ( TimestampProperty ) | ( VariableDateProperty );
Sluggable: ( Announcement ) | ( AssetAudio ) | ( AssetDocument ) | ( AssetImage ) | ( AssetPdf ) | ( AssetUnknown ) | ( AssetVideo ) | ( BlurbTemplateDefinition ) | ( BlurbTemplateInstance ) | ( Omit & { parent?: Maybe } ) | ( CollectionAttribution ) | ( CollectionContribution ) | ( Community ) | ( ContextualPermission ) | ( ContributionRoleConfiguration ) | ( ContributorCollectionAttribution ) | ( ContributorItemAttribution ) | ( ContributorListTemplateDefinition ) | ( ContributorListTemplateInstance ) | ( ContributorUserLink ) | ( ControlledVocabulary ) | ( ControlledVocabularyItem ) | ( ControlledVocabularySource ) | ( DepositorAgreement ) | ( DepositorAgreementTransition ) | ( DepositorRequest ) | ( DepositorRequestTransition ) | ( DescendantListTemplateDefinition ) | ( DescendantListTemplateInstance ) | ( DetailTemplateDefinition ) | ( DetailTemplateInstance ) | ( EntityLink ) | ( HarvestAttempt ) | ( HarvestAttemptEntityStatus ) | ( HarvestAttemptRecordStatus ) | ( HarvestEntity ) | ( HarvestError ) | ( HarvestMapping ) | ( HarvestMessage ) | ( HarvestMetadataMapping ) | ( HarvestRecord ) | ( HarvestSet ) | ( HarvestSource ) | ( Omit & { templates: Array } ) | ( Omit & { templates: Array } ) | ( HeroTemplateDefinition ) | ( HeroTemplateInstance ) | ( Omit
- & { parent?: Maybe } ) | ( ItemAttribution ) | ( ItemContribution ) | ( LinkListTemplateDefinition ) | ( LinkListTemplateInstance ) | ( Omit & { templates: Array } ) | ( Omit & { templates: Array } ) | ( ListItemTemplateDefinition ) | ( ListItemTemplateInstance ) | ( Omit & { templates: Array } ) | ( Omit & { templates: Array } ) | ( Omit & { templates: Array } ) | ( Omit & { templates: Array } ) | ( MetadataTemplateDefinition ) | ( MetadataTemplateInstance ) | ( Omit & { templates: Array } ) | ( Omit & { templates: Array } ) | ( NavigationTemplateDefinition ) | ( NavigationTemplateInstance ) | ( Ordering ) | ( OrderingEntry ) | ( OrderingTemplateDefinition ) | ( OrderingTemplateInstance ) | ( OrganizationContributor ) | ( PageListTemplateDefinition ) | ( PageListTemplateInstance ) | ( Permalink ) | ( PersonContributor ) | ( Role ) | ( SchemaDefinition ) | ( SchemaVersion ) | ( SearchResult ) | ( Submission ) | ( SubmissionBatchPublication ) | ( SubmissionBatchPublicationTransition ) | ( SubmissionComment ) | ( SubmissionDepositTarget ) | ( SubmissionPublication ) | ( SubmissionPublicationTransition ) | ( SubmissionReview ) | ( SubmissionReviewTransition ) | ( SubmissionTarget ) | ( SubmissionTargetReviewer ) | ( SubmissionTargetTransition ) | ( SubmissionTransition ) | ( Omit & { templates: Array } ) | ( Omit & { templates: Array } ) | ( SupplementaryTemplateDefinition ) | ( SupplementaryTemplateInstance ) | ( TemplateInstanceSibling ) | ( User ) | ( UserCollectionAccessGrant ) | ( UserCommunityAccessGrant ) | ( UserGroup ) | ( UserGroupCollectionAccessGrant ) | ( UserGroupCommunityAccessGrant ) | ( UserGroupItemAccessGrant ) | ( UserItemAccessGrant );
- StandardMutationPayload: ( Omit & { entity?: Maybe } ) | ( Omit & { entity?: Maybe } ) | ( ContributorClaimPayload ) | ( ContributorMergePayload ) | ( ContributorUserLinkDestroyPayload ) | ( ContributorUserLinkUpsertPayload ) | ( ControlledVocabularyDestroyPayload ) | ( ControlledVocabularySourceUpdatePayload ) | ( ControlledVocabularyUpsertPayload ) | ( CreateAnnouncementPayload ) | ( Omit & { asset?: Maybe } ) | ( CreateCollectionPayload ) | ( CreateCommunityPayload ) | ( CreateItemPayload ) | ( CreateOrderingPayload ) | ( CreateOrganizationContributorPayload ) | ( CreatePagePayload ) | ( CreatePersonContributorPayload ) | ( CreateRolePayload ) | ( DepositorAgreementAcceptPayload ) | ( DepositorAgreementResetAllPayload ) | ( DepositorAgreementResetPayload ) | ( DepositorRequestChangeStatePayload ) | ( DepositorRequestCreatePayload ) | ( DestroyAnnouncementPayload ) | ( DestroyAssetPayload ) | ( DestroyCollectionPayload ) | ( DestroyCommunityPayload ) | ( DestroyContributionPayload ) | ( DestroyContributorPayload ) | ( DestroyEntityLinkPayload ) | ( DestroyItemPayload ) | ( DestroyOrderingPayload ) | ( DestroyPagePayload ) | ( EntityPurgePayload ) | ( FrontendCacheRevalidateEntityPayload ) | ( FrontendCacheRevalidateInstancePayload ) | ( Omit & { entity?: Maybe } ) | ( HarvestAttemptFromMappingPayload ) | ( HarvestAttemptFromSourcePayload ) | ( HarvestAttemptPruneEntitiesPayload ) | ( HarvestMappingCreatePayload ) | ( HarvestMappingDestroyPayload ) | ( HarvestMappingUpdatePayload ) | ( HarvestMetadataMappingCreatePayload ) | ( HarvestMetadataMappingDestroyPayload ) | ( HarvestSourceCreatePayload ) | ( HarvestSourceDestroyPayload ) | ( HarvestSourcePruneEntitiesPayload ) | ( HarvestSourceUpdatePayload ) | ( LinkEntityPayload ) | ( PermalinkCreatePayload ) | ( PermalinkDestroyPayload ) | ( PermalinkUpdatePayload ) | ( PreviewSlotPayload ) | ( Omit & { entity?: Maybe } ) | ( Omit & { child?: Maybe } ) | ( ResetOrderingPayload ) | ( Omit & { entity?: Maybe } ) | ( SubmissionBatchPublishPayload ) | ( SubmissionChangeStatePayload ) | ( SubmissionCommentCreatePayload ) | ( SubmissionCommentDestroyPayload ) | ( SubmissionCommentUpdatePayload ) | ( SubmissionCreatePayload ) | ( SubmissionLeaveReviewPayload ) | ( SubmissionPublishPayload ) | ( SubmissionRequestReviewPayload ) | ( SubmissionTargetClosePayload ) | ( SubmissionTargetConfigurePayload ) | ( SubmissionTargetOpenPayload ) | ( SubmissionTargetReviewerCreatePayload ) | ( SubmissionTargetReviewerDestroyPayload ) | ( UpdateAnnouncementPayload ) | ( Omit & { asset?: Maybe } ) | ( Omit & { asset?: Maybe } ) | ( UpdateCollectionPayload ) | ( UpdateCommunityPayload ) | ( Omit & { contribution?: Maybe } ) | ( UpdateGlobalConfigurationPayload ) | ( UpdateItemPayload ) | ( UpdateOrderingPayload ) | ( UpdateOrganizationContributorPayload ) | ( UpdatePagePayload ) | ( UpdatePersonContributorPayload ) | ( UpdateRolePayload ) | ( UpdateUserPayload ) | ( UpdateViewerSettingsPayload ) | ( Omit & { contribution?: Maybe } ) | ( UserResetPasswordPayload );
+ StandardMutationPayload: ( Omit & { entity?: Maybe } ) | ( Omit & { entity?: Maybe } ) | ( ContributorClaimPayload ) | ( ContributorMergePayload ) | ( ContributorUserLinkDestroyPayload ) | ( ContributorUserLinkUpsertPayload ) | ( ControlledVocabularyDestroyPayload ) | ( ControlledVocabularySourceUpdatePayload ) | ( ControlledVocabularyUpsertPayload ) | ( CreateAnnouncementPayload ) | ( Omit & { asset?: Maybe } ) | ( CreateCollectionPayload ) | ( CreateCommunityPayload ) | ( CreateItemPayload ) | ( CreateOrderingPayload ) | ( CreateOrganizationContributorPayload ) | ( CreatePagePayload ) | ( CreatePersonContributorPayload ) | ( CreateRolePayload ) | ( DepositorAgreementAcceptPayload ) | ( DepositorAgreementResetAllPayload ) | ( DepositorAgreementResetPayload ) | ( DepositorRequestChangeStatePayload ) | ( DepositorRequestCreatePayload ) | ( DestroyAnnouncementPayload ) | ( DestroyAssetPayload ) | ( DestroyCollectionPayload ) | ( DestroyCommunityPayload ) | ( DestroyContributionPayload ) | ( DestroyContributorPayload ) | ( DestroyEntityLinkPayload ) | ( DestroyItemPayload ) | ( DestroyOrderingPayload ) | ( DestroyPagePayload ) | ( EntityPurgePayload ) | ( FrontendCacheRevalidateEntityPayload ) | ( FrontendCacheRevalidateInstancePayload ) | ( Omit & { entity?: Maybe } ) | ( HarvestAttemptFromMappingPayload ) | ( HarvestAttemptFromSourcePayload ) | ( HarvestAttemptPruneEntitiesPayload ) | ( HarvestMappingCreatePayload ) | ( HarvestMappingDestroyPayload ) | ( HarvestMappingUpdatePayload ) | ( HarvestMetadataMappingCreatePayload ) | ( HarvestMetadataMappingDestroyPayload ) | ( HarvestSourceCreatePayload ) | ( HarvestSourceDestroyPayload ) | ( HarvestSourcePruneEntitiesPayload ) | ( HarvestSourceUpdatePayload ) | ( LinkEntityPayload ) | ( PermalinkCreatePayload ) | ( PermalinkDestroyPayload ) | ( PermalinkUpdatePayload ) | ( PreviewSlotPayload ) | ( Omit & { entity?: Maybe } ) | ( Omit & { child?: Maybe } ) | ( ResetOrderingPayload ) | ( Omit & { entity?: Maybe } ) | ( SubmissionBatchPublishPayload ) | ( SubmissionChangeStatePayload ) | ( SubmissionCommentCreatePayload ) | ( SubmissionCommentDestroyPayload ) | ( SubmissionCommentUpdatePayload ) | ( SubmissionCreatePayload ) | ( SubmissionDestroyPayload ) | ( SubmissionLeaveReviewPayload ) | ( SubmissionPublishPayload ) | ( SubmissionRequestReviewPayload ) | ( SubmissionTargetClosePayload ) | ( SubmissionTargetConfigurePayload ) | ( SubmissionTargetOpenPayload ) | ( SubmissionTargetReviewerCreatePayload ) | ( SubmissionTargetReviewerDestroyPayload ) | ( UpdateAnnouncementPayload ) | ( Omit & { asset?: Maybe } ) | ( Omit & { asset?: Maybe } ) | ( UpdateCollectionPayload ) | ( UpdateCommunityPayload ) | ( Omit & { contribution?: Maybe } ) | ( UpdateGlobalConfigurationPayload ) | ( UpdateItemPayload ) | ( UpdateOrderingPayload ) | ( UpdateOrganizationContributorPayload ) | ( UpdatePagePayload ) | ( UpdatePersonContributorPayload ) | ( UpdateRolePayload ) | ( UpdateUserPayload ) | ( UpdateViewerSettingsPayload ) | ( Omit & { contribution?: Maybe } ) | ( UserResetPasswordPayload );
Submittable: ( Omit & { parent?: Maybe } ) | ( Community ) | ( Omit
- & { parent?: Maybe } );
TemplateDefinition: ( BlurbTemplateDefinition ) | ( ContributorListTemplateDefinition ) | ( DescendantListTemplateDefinition ) | ( DetailTemplateDefinition ) | ( HeroTemplateDefinition ) | ( LinkListTemplateDefinition ) | ( ListItemTemplateDefinition ) | ( MetadataTemplateDefinition ) | ( NavigationTemplateDefinition ) | ( OrderingTemplateDefinition ) | ( PageListTemplateDefinition ) | ( SupplementaryTemplateDefinition );
TemplateHasContributionList: ( ContributorListTemplateInstance );
@@ -29722,6 +29799,8 @@ export type ResolversTypes = {
SubmissionCreatePayload: ResolverTypeWrapper;
SubmissionDepositMode: SubmissionDepositMode;
SubmissionDepositTarget: ResolverTypeWrapper;
+ SubmissionDestroyInput: SubmissionDestroyInput;
+ SubmissionDestroyPayload: ResolverTypeWrapper;
SubmissionEdge: ResolverTypeWrapper;
SubmissionFilterInput: SubmissionFilterInput;
SubmissionLeaveReviewInput: SubmissionLeaveReviewInput;
@@ -30467,6 +30546,8 @@ export type ResolversParentTypes = {
SubmissionCreateInput: SubmissionCreateInput;
SubmissionCreatePayload: SubmissionCreatePayload;
SubmissionDepositTarget: SubmissionDepositTarget;
+ SubmissionDestroyInput: SubmissionDestroyInput;
+ SubmissionDestroyPayload: SubmissionDestroyPayload;
SubmissionEdge: SubmissionEdge;
SubmissionFilterInput: SubmissionFilterInput;
SubmissionLeaveReviewInput: SubmissionLeaveReviewInput;
@@ -31327,6 +31408,7 @@ export type ChildEntityResolvers;
canDestroy?: Resolver;
canManageAccess?: Resolver;
+ canPreview?: Resolver;
canPurge?: Resolver;
canReparent?: Resolver;
canRevalidate?: Resolver;
@@ -31412,6 +31494,7 @@ export type CollectionResolvers;
canDestroy?: Resolver;
canManageAccess?: Resolver;
+ canPreview?: Resolver;
canPurge?: Resolver;
canReparent?: Resolver;
canRevalidate?: Resolver;
@@ -31609,6 +31692,7 @@ export type CommunityResolvers;
canDestroy?: Resolver;
canManageAccess?: Resolver;
+ canPreview?: Resolver;
canPurge?: Resolver;
canReparent?: Resolver;
canRevalidate?: Resolver;
@@ -32728,7 +32812,7 @@ export type DestroyItemPayloadResolvers = {
- __resolveType: TypeResolveFn<'ContributorUserLinkDestroyPayload' | 'ControlledVocabularyDestroyPayload' | 'DestroyAnnouncementPayload' | 'DestroyAssetPayload' | 'DestroyCollectionPayload' | 'DestroyCommunityPayload' | 'DestroyContributionPayload' | 'DestroyContributorPayload' | 'DestroyEntityLinkPayload' | 'DestroyItemPayload' | 'DestroyOrderingPayload' | 'DestroyPagePayload' | 'EntityPurgePayload' | 'HarvestMappingDestroyPayload' | 'HarvestMetadataMappingDestroyPayload' | 'HarvestSourceDestroyPayload' | 'PermalinkDestroyPayload' | 'SubmissionCommentDestroyPayload' | 'SubmissionTargetReviewerDestroyPayload', ParentType, ContextType>;
+ __resolveType: TypeResolveFn<'ContributorUserLinkDestroyPayload' | 'ControlledVocabularyDestroyPayload' | 'DestroyAnnouncementPayload' | 'DestroyAssetPayload' | 'DestroyCollectionPayload' | 'DestroyCommunityPayload' | 'DestroyContributionPayload' | 'DestroyContributorPayload' | 'DestroyEntityLinkPayload' | 'DestroyItemPayload' | 'DestroyOrderingPayload' | 'DestroyPagePayload' | 'EntityPurgePayload' | 'HarvestMappingDestroyPayload' | 'HarvestMetadataMappingDestroyPayload' | 'HarvestSourceDestroyPayload' | 'PermalinkDestroyPayload' | 'SubmissionCommentDestroyPayload' | 'SubmissionDestroyPayload' | 'SubmissionTargetReviewerDestroyPayload', ParentType, ContextType>;
attributeErrors?: Resolver, ParentType, ContextType>;
destroyed?: Resolver, ParentType, ContextType>;
destroyedId?: Resolver, ParentType, ContextType>;
@@ -32887,6 +32971,7 @@ export type EntityResolvers;
canDestroy?: Resolver;
canManageAccess?: Resolver;
+ canPreview?: Resolver;
canPurge?: Resolver;
canReparent?: Resolver;
canRevalidate?: Resolver;
@@ -33034,6 +33119,7 @@ export type EntityPermissionsResolvers;
canDeposit?: Resolver;
canManageAccess?: Resolver;
+ canPreview?: Resolver;
canPurge?: Resolver;
canReparent?: Resolver;
canRevalidate?: Resolver;
@@ -34091,6 +34177,7 @@ export type ItemResolvers;
canDestroy?: Resolver;
canManageAccess?: Resolver;
+ canPreview?: Resolver;
canPurge?: Resolver;
canReparent?: Resolver;
canRevalidate?: Resolver;
@@ -34735,6 +34822,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>;
submissionCommentUpdate?: Resolver, ParentType, ContextType, RequireFields>;
submissionCreate?: Resolver, ParentType, ContextType, RequireFields>;
+ submissionDestroy?: Resolver, ParentType, ContextType, RequireFields>;
submissionLeaveReview?: Resolver, ParentType, ContextType, RequireFields>;
submissionPublish?: Resolver, ParentType, ContextType, RequireFields>;
submissionRequestReview?: Resolver, ParentType, ContextType, RequireFields>;
@@ -35978,7 +36066,7 @@ export type SluggableResolvers = {
- __resolveType: TypeResolveFn<'AlterSchemaVersionPayload' | 'ApplySchemaPropertiesPayload' | 'ContributorClaimPayload' | 'ContributorMergePayload' | 'ContributorUserLinkDestroyPayload' | 'ContributorUserLinkUpsertPayload' | 'ControlledVocabularyDestroyPayload' | 'ControlledVocabularySourceUpdatePayload' | 'ControlledVocabularyUpsertPayload' | 'CreateAnnouncementPayload' | 'CreateAssetPayload' | 'CreateCollectionPayload' | 'CreateCommunityPayload' | 'CreateItemPayload' | 'CreateOrderingPayload' | 'CreateOrganizationContributorPayload' | 'CreatePagePayload' | 'CreatePersonContributorPayload' | 'CreateRolePayload' | 'DepositorAgreementAcceptPayload' | 'DepositorAgreementResetAllPayload' | 'DepositorAgreementResetPayload' | 'DepositorRequestChangeStatePayload' | 'DepositorRequestCreatePayload' | 'DestroyAnnouncementPayload' | 'DestroyAssetPayload' | 'DestroyCollectionPayload' | 'DestroyCommunityPayload' | 'DestroyContributionPayload' | 'DestroyContributorPayload' | 'DestroyEntityLinkPayload' | 'DestroyItemPayload' | 'DestroyOrderingPayload' | 'DestroyPagePayload' | 'EntityPurgePayload' | 'FrontendCacheRevalidateEntityPayload' | 'FrontendCacheRevalidateInstancePayload' | 'GrantAccessPayload' | 'HarvestAttemptFromMappingPayload' | 'HarvestAttemptFromSourcePayload' | 'HarvestAttemptPruneEntitiesPayload' | 'HarvestMappingCreatePayload' | 'HarvestMappingDestroyPayload' | 'HarvestMappingUpdatePayload' | 'HarvestMetadataMappingCreatePayload' | 'HarvestMetadataMappingDestroyPayload' | 'HarvestSourceCreatePayload' | 'HarvestSourceDestroyPayload' | 'HarvestSourcePruneEntitiesPayload' | 'HarvestSourceUpdatePayload' | 'LinkEntityPayload' | 'PermalinkCreatePayload' | 'PermalinkDestroyPayload' | 'PermalinkUpdatePayload' | 'PreviewSlotPayload' | 'RenderLayoutsPayload' | 'ReparentEntityPayload' | 'ResetOrderingPayload' | 'RevokeAccessPayload' | 'SubmissionBatchPublishPayload' | 'SubmissionChangeStatePayload' | 'SubmissionCommentCreatePayload' | 'SubmissionCommentDestroyPayload' | 'SubmissionCommentUpdatePayload' | 'SubmissionCreatePayload' | 'SubmissionLeaveReviewPayload' | 'SubmissionPublishPayload' | 'SubmissionRequestReviewPayload' | 'SubmissionTargetClosePayload' | 'SubmissionTargetConfigurePayload' | 'SubmissionTargetOpenPayload' | 'SubmissionTargetReviewerCreatePayload' | 'SubmissionTargetReviewerDestroyPayload' | 'UpdateAnnouncementPayload' | 'UpdateAssetAttachmentPayload' | 'UpdateAssetPayload' | 'UpdateCollectionPayload' | 'UpdateCommunityPayload' | 'UpdateContributionPayload' | 'UpdateGlobalConfigurationPayload' | 'UpdateItemPayload' | 'UpdateOrderingPayload' | 'UpdateOrganizationContributorPayload' | 'UpdatePagePayload' | 'UpdatePersonContributorPayload' | 'UpdateRolePayload' | 'UpdateUserPayload' | 'UpdateViewerSettingsPayload' | 'UpsertContributionPayload' | 'UserResetPasswordPayload', ParentType, ContextType>;
+ __resolveType: TypeResolveFn<'AlterSchemaVersionPayload' | 'ApplySchemaPropertiesPayload' | 'ContributorClaimPayload' | 'ContributorMergePayload' | 'ContributorUserLinkDestroyPayload' | 'ContributorUserLinkUpsertPayload' | 'ControlledVocabularyDestroyPayload' | 'ControlledVocabularySourceUpdatePayload' | 'ControlledVocabularyUpsertPayload' | 'CreateAnnouncementPayload' | 'CreateAssetPayload' | 'CreateCollectionPayload' | 'CreateCommunityPayload' | 'CreateItemPayload' | 'CreateOrderingPayload' | 'CreateOrganizationContributorPayload' | 'CreatePagePayload' | 'CreatePersonContributorPayload' | 'CreateRolePayload' | 'DepositorAgreementAcceptPayload' | 'DepositorAgreementResetAllPayload' | 'DepositorAgreementResetPayload' | 'DepositorRequestChangeStatePayload' | 'DepositorRequestCreatePayload' | 'DestroyAnnouncementPayload' | 'DestroyAssetPayload' | 'DestroyCollectionPayload' | 'DestroyCommunityPayload' | 'DestroyContributionPayload' | 'DestroyContributorPayload' | 'DestroyEntityLinkPayload' | 'DestroyItemPayload' | 'DestroyOrderingPayload' | 'DestroyPagePayload' | 'EntityPurgePayload' | 'FrontendCacheRevalidateEntityPayload' | 'FrontendCacheRevalidateInstancePayload' | 'GrantAccessPayload' | 'HarvestAttemptFromMappingPayload' | 'HarvestAttemptFromSourcePayload' | 'HarvestAttemptPruneEntitiesPayload' | 'HarvestMappingCreatePayload' | 'HarvestMappingDestroyPayload' | 'HarvestMappingUpdatePayload' | 'HarvestMetadataMappingCreatePayload' | 'HarvestMetadataMappingDestroyPayload' | 'HarvestSourceCreatePayload' | 'HarvestSourceDestroyPayload' | 'HarvestSourcePruneEntitiesPayload' | 'HarvestSourceUpdatePayload' | 'LinkEntityPayload' | 'PermalinkCreatePayload' | 'PermalinkDestroyPayload' | 'PermalinkUpdatePayload' | 'PreviewSlotPayload' | 'RenderLayoutsPayload' | 'ReparentEntityPayload' | 'ResetOrderingPayload' | 'RevokeAccessPayload' | 'SubmissionBatchPublishPayload' | 'SubmissionChangeStatePayload' | 'SubmissionCommentCreatePayload' | 'SubmissionCommentDestroyPayload' | 'SubmissionCommentUpdatePayload' | 'SubmissionCreatePayload' | 'SubmissionDestroyPayload' | 'SubmissionLeaveReviewPayload' | 'SubmissionPublishPayload' | 'SubmissionRequestReviewPayload' | 'SubmissionTargetClosePayload' | 'SubmissionTargetConfigurePayload' | 'SubmissionTargetOpenPayload' | 'SubmissionTargetReviewerCreatePayload' | 'SubmissionTargetReviewerDestroyPayload' | 'UpdateAnnouncementPayload' | 'UpdateAssetAttachmentPayload' | 'UpdateAssetPayload' | 'UpdateCollectionPayload' | 'UpdateCommunityPayload' | 'UpdateContributionPayload' | 'UpdateGlobalConfigurationPayload' | 'UpdateItemPayload' | 'UpdateOrderingPayload' | 'UpdateOrganizationContributorPayload' | 'UpdatePagePayload' | 'UpdatePersonContributorPayload' | 'UpdateRolePayload' | 'UpdateUserPayload' | 'UpdateViewerSettingsPayload' | 'UpsertContributionPayload' | 'UserResetPasswordPayload', ParentType, ContextType>;
attributeErrors?: Resolver, ParentType, ContextType>;
errors?: Resolver, ParentType, ContextType>;
globalErrors?: Resolver, ParentType, ContextType>;
@@ -36201,6 +36289,17 @@ export type SubmissionDepositTargetResolvers;
};
+export type SubmissionDestroyPayloadResolvers = {
+ attributeErrors?: Resolver, ParentType, ContextType>;
+ clientMutationId?: Resolver, ParentType, ContextType>;
+ destroyed?: Resolver, ParentType, ContextType>;
+ destroyedId?: Resolver, ParentType, ContextType>;
+ errors?: Resolver, ParentType, ContextType>;
+ globalErrors?: Resolver, ParentType, ContextType>;
+ haltCode?: Resolver, ParentType, ContextType>;
+ __isTypeOf?: IsTypeOfResolverFn;
+};
+
export type SubmissionEdgeResolvers = {
cursor?: Resolver;
node?: Resolver;
@@ -37085,6 +37184,7 @@ export type UserResolvers;
canClaimContributor?: Resolver;
canDestroy?: Resolver;
+ canPreview?: Resolver