From 1766896b906fc830329e6dbe43a1602e2d691edb Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Tue, 5 Aug 2025 15:33:49 +0200 Subject: [PATCH] Allow edition of some document fields Signed-off-by: Sacha Al Himdani --- .../components/form/DocumentTypeOptions.tsx | 17 ++ ...tCenterAuditGraphUpdateMutation.graphql.ts | 175 ++++++++---- ...CenterVendorGraphUpdateMutation.graphql.ts | 156 +++++++---- .../documents/DocumentDetailPage.tsx | 250 +++++++++++++++++- ...ocumentDetailPageUpdateMutation.graphql.ts | 144 ++++++++++ .../dialogs/CreateDocumentDialog.tsx | 12 +- .../UpdateDocumentDialogMutation.graphql.ts | 144 ++++++++++ 7 files changed, 784 insertions(+), 114 deletions(-) create mode 100644 apps/console/src/components/form/DocumentTypeOptions.tsx create mode 100644 apps/console/src/pages/organizations/documents/__generated__/DocumentDetailPageUpdateMutation.graphql.ts create mode 100644 apps/console/src/pages/organizations/documents/dialogs/__generated__/UpdateDocumentDialogMutation.graphql.ts diff --git a/apps/console/src/components/form/DocumentTypeOptions.tsx b/apps/console/src/components/form/DocumentTypeOptions.tsx new file mode 100644 index 000000000..ed357b814 --- /dev/null +++ b/apps/console/src/components/form/DocumentTypeOptions.tsx @@ -0,0 +1,17 @@ +import { useTranslate } from "@probo/i18n"; +import { Option } from "@probo/ui"; +import { documentTypes, getDocumentTypeLabel } from "@probo/helpers"; + +export function DocumentTypeOptions() { + const { __ } = useTranslate(); + + return ( + <> + {documentTypes.map((type) => ( + + ))} + + ); +} diff --git a/apps/console/src/hooks/graph/__generated__/TrustCenterAuditGraphUpdateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/TrustCenterAuditGraphUpdateMutation.graphql.ts index 943b26edb..de001b381 100644 --- a/apps/console/src/hooks/graph/__generated__/TrustCenterAuditGraphUpdateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/TrustCenterAuditGraphUpdateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<92803a3585816e3db509c46a86d083e9>> + * @generated SignedSource<<2a5e6cd48ca8a8fd7fe448c3a6190ccc>> * @lightSyntaxTransform * @nogrep */ @@ -9,6 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; export type AuditState = "COMPLETED" | "IN_PROGRESS" | "NOT_STARTED" | "OUTDATED" | "REJECTED"; export type UpdateAuditInput = { id: string; @@ -25,6 +26,7 @@ export type TrustCenterAuditGraphUpdateMutation$data = { readonly audit: { readonly id: string; readonly showOnTrustCenter: boolean; + readonly " $fragmentSpreads": FragmentRefs<"TrustCenterAuditsCardFragment">; }; }; }; @@ -43,55 +45,62 @@ var v0 = [ ], v1 = [ { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "UpdateAuditPayload", - "kind": "LinkedField", - "name": "updateAudit", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Audit", - "kind": "LinkedField", - "name": "audit", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "showOnTrustCenter", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null + "kind": "Variable", + "name": "input", + "variableName": "input" } -]; +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "showOnTrustCenter", + "storageKey": null +}; return { "fragment": { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, "name": "TrustCenterAuditGraphUpdateMutation", - "selections": (v1/*: any*/), + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateAuditPayload", + "kind": "LinkedField", + "name": "updateAudit", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Audit", + "kind": "LinkedField", + "name": "audit", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "TrustCenterAuditsCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], "type": "Mutation", "abstractKey": null }, @@ -100,19 +109,91 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "TrustCenterAuditGraphUpdateMutation", - "selections": (v1/*: any*/) + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateAuditPayload", + "kind": "LinkedField", + "name": "updateAudit", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Audit", + "kind": "LinkedField", + "name": "audit", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + (v2/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validFrom", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validUntil", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ] }, "params": { - "cacheID": "1ecc930abe6daebb184b1e55ccebc65c", + "cacheID": "9cb78f25506e1a67b5f29d4f742f8773", "id": null, "metadata": {}, "name": "TrustCenterAuditGraphUpdateMutation", "operationKind": "mutation", - "text": "mutation TrustCenterAuditGraphUpdateMutation(\n $input: UpdateAuditInput!\n) {\n updateAudit(input: $input) {\n audit {\n id\n showOnTrustCenter\n }\n }\n}\n" + "text": "mutation TrustCenterAuditGraphUpdateMutation(\n $input: UpdateAuditInput!\n) {\n updateAudit(input: $input) {\n audit {\n id\n showOnTrustCenter\n ...TrustCenterAuditsCardFragment\n }\n }\n}\n\nfragment TrustCenterAuditsCardFragment on Audit {\n id\n framework {\n name\n id\n }\n validFrom\n validUntil\n state\n showOnTrustCenter\n createdAt\n}\n" } }; })(); -(node as any).hash = "ebf90264b72bb993d60f48d1d3fc4fb7"; +(node as any).hash = "1e92374989ecf8f439d069eb003e4e4e"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/TrustCenterVendorGraphUpdateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/TrustCenterVendorGraphUpdateMutation.graphql.ts index 2e150807b..481ee137c 100644 --- a/apps/console/src/hooks/graph/__generated__/TrustCenterVendorGraphUpdateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/TrustCenterVendorGraphUpdateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<012c4e605372a38efef4780c63d40888>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,6 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL"; export type UpdateVendorInput = { businessAssociateAgreementUrl?: string | null | undefined; @@ -40,6 +41,7 @@ export type TrustCenterVendorGraphUpdateMutation$data = { readonly vendor: { readonly id: string; readonly showOnTrustCenter: boolean; + readonly " $fragmentSpreads": FragmentRefs<"TrustCenterVendorsCardFragment">; }; }; }; @@ -58,55 +60,62 @@ var v0 = [ ], v1 = [ { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "UpdateVendorPayload", - "kind": "LinkedField", - "name": "updateVendor", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Vendor", - "kind": "LinkedField", - "name": "vendor", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "showOnTrustCenter", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null + "kind": "Variable", + "name": "input", + "variableName": "input" } -]; +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "showOnTrustCenter", + "storageKey": null +}; return { "fragment": { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, "name": "TrustCenterVendorGraphUpdateMutation", - "selections": (v1/*: any*/), + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateVendorPayload", + "kind": "LinkedField", + "name": "updateVendor", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "vendor", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "TrustCenterVendorsCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], "type": "Mutation", "abstractKey": null }, @@ -115,19 +124,72 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "TrustCenterVendorGraphUpdateMutation", - "selections": (v1/*: any*/) + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateVendorPayload", + "kind": "LinkedField", + "name": "updateVendor", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "vendor", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ] }, "params": { - "cacheID": "44e483af96833ea9efb1567fa5983282", + "cacheID": "92823eef2e21ea731d8f37dbf9cd5da3", "id": null, "metadata": {}, "name": "TrustCenterVendorGraphUpdateMutation", "operationKind": "mutation", - "text": "mutation TrustCenterVendorGraphUpdateMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n id\n showOnTrustCenter\n }\n }\n}\n" + "text": "mutation TrustCenterVendorGraphUpdateMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n id\n showOnTrustCenter\n ...TrustCenterVendorsCardFragment\n }\n }\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n}\n" } }; })(); -(node as any).hash = "e561918e67c08f1e3fae85dfb04591d1"; +(node as any).hash = "2b4395821fcfbd30bb06448db5429289"; export default node; diff --git a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx index 6a074dbc7..afe8ef4a2 100644 --- a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx @@ -18,6 +18,7 @@ import type { DocumentDetailPageDocumentFragment$key, } from "./__generated__/DocumentDetailPageDocumentFragment.graphql"; import type { DocumentDetailPageExportPDFMutation } from "./__generated__/DocumentDetailPageExportPDFMutation.graphql"; +import type { DocumentDetailPageUpdateMutation } from "./__generated__/DocumentDetailPageUpdateMutation.graphql"; import { useTranslate } from "@probo/i18n"; import { ActionDropdown, @@ -34,6 +35,8 @@ import { IconClock, IconPencil, IconTrashCan, + IconCrossLargeX, + Input, PageHeader, PropertyRow, TabBadge, @@ -43,7 +46,7 @@ import { } from "@probo/ui"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; -import { getDocumentTypeLabel, sprintf } from "@probo/helpers"; +import { getDocumentTypeLabel, sprintf, documentTypes } from "@probo/helpers"; import { Link, Outlet, @@ -52,9 +55,14 @@ import { useParams, } from "react-router"; import UpdateVersionDialog from "./dialogs/UpdateVersionDialog"; -import { useRef } from "react"; +import { useRef, useState } from "react"; import type { NodeOf } from "/types.ts"; import clsx from "clsx"; +import { PeopleSelectField } from "/components/form/PeopleSelectField"; +import { ControlledField } from "/components/form/ControlledField"; +import { DocumentTypeOptions } from "/components/form/DocumentTypeOptions"; +import { z } from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; type Props = { queryRef: PreloadedQuery; @@ -128,6 +136,28 @@ const exportDocumentVersionPDFMutation = graphql` } `; +const updateDocumentMutation = graphql` + mutation DocumentDetailPageUpdateMutation($input: UpdateDocumentInput!) { + updateDocument(input: $input) { + document { + id + title + documentType + owner { + id + fullName + } + } + } + } +`; + +const documentUpdateSchema = z.object({ + title: z.string().min(1, "Title is required").max(255), + ownerId: z.string().min(1, "Owner is required"), + documentType: z.enum(documentTypes), +}); + export default function DocumentDetailPage(props: Props) { const { versionId } = useParams<{ versionId?: string }>(); const node = usePreloadedQuery(documentNodeQuery, props.queryRef).node; @@ -138,6 +168,10 @@ export default function DocumentDetailPage(props: Props) { const { __, dateFormat } = useTranslate(); const organizationId = useOrganizationId(); const navigate = useNavigate(); + + const [isEditingTitle, setIsEditingTitle] = useState(false); + const [isEditingOwner, setIsEditingOwner] = useState(false); + const [isEditingType, setIsEditingType] = useState(false); const versions = document.versions.edges.map((edge) => edge.node); const currentVersion = document.versions.edges.find((v) => v.node.id === versionId)?.node ?? @@ -161,10 +195,73 @@ export default function DocumentDetailPage(props: Props) { errorMessage: __("Failed to generate PDF. Please try again."), } ); + const [updateDocument, isUpdatingDocument] = useMutationWithToasts( + updateDocumentMutation, + { + successMessage: __("Document updated successfully."), + errorMessage: __("Failed to update document. Please try again."), + } + ); const versionConnectionId = document.versions.__id; + const { register, control, handleSubmit, reset } = useFormWithSchema(documentUpdateSchema, { + defaultValues: { + title: document.title, + ownerId: document.owner?.id || "", + documentType: document.documentType, + }, + }); + usePageTitle(document.title); + const handleUpdateTitle = (data: { title: string }) => { + updateDocument({ + variables: { + input: { + id: document.id, + title: data.title, + }, + }, + onSuccess: () => { + setIsEditingTitle(false); + }, + }); + }; + + const handleUpdateOwner = (data: { ownerId: string }) => { + updateDocument({ + variables: { + input: { + id: document.id, + ownerId: data.ownerId, + }, + }, + onSuccess: () => { + setIsEditingOwner(false); + }, + }); + }; + + const handleUpdateDocumentType = (data: { documentType: typeof documentTypes[number] }) => { + updateDocument({ + variables: { + input: { + id: document.id, + documentType: data.documentType, + }, + }, + onSuccess: () => { + setIsEditingType(false); + loadQuery( + props.queryRef.environment, + documentNodeQuery, + props.queryRef.variables, + { fetchPolicy: "network-only" } + ); + }, + }); + }; + const handlePublish = () => { publishDocumentVersion({ variables: { @@ -313,7 +410,52 @@ export default function DocumentDetailPage(props: Props) { - + + { + if (e.key === "Escape") { + setIsEditingTitle(false); + reset(); + } + if (e.key === "Enter") { + handleSubmit(handleUpdateTitle)(); + } + }} + /> +