From c493e317458d760bc59ffd666f3b3da74001fbca Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Tue, 10 Mar 2026 16:10:23 +0100 Subject: [PATCH] Fix documents UI - Fix redirection after document deletion in detail page - Fix refetch after publication - Return proper error when trying to update a published document Signed-off-by: Sacha Al Himdani --- .../DocumentGraphDeleteMutation.graphql.ts | 55 ++++++++++++------- apps/console/src/hooks/graph/DocumentGraph.ts | 7 ++- .../documents/DocumentLayout.tsx | 7 ++- .../documents/DocumentLayoutLoader.tsx | 11 +++- .../_components/DocumentActionsDropdown.tsx | 28 ++++++++-- .../_components/UpdateVersionDialog.tsx | 5 +- pkg/probo/document_service.go | 9 ++- pkg/server/api/console/v1/v1_resolver.go | 12 +++- 8 files changed, 98 insertions(+), 36 deletions(-) diff --git a/apps/console/src/__generated__/core/DocumentGraphDeleteMutation.graphql.ts b/apps/console/src/__generated__/core/DocumentGraphDeleteMutation.graphql.ts index cf7b24994..2d62ca753 100644 --- a/apps/console/src/__generated__/core/DocumentGraphDeleteMutation.graphql.ts +++ b/apps/console/src/__generated__/core/DocumentGraphDeleteMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<6b7916e6ff46a629bf97a7b072db2d8b>> + * @generated SignedSource<<64c2c92a2a0806f34243f8fc9904167c>> * @lightSyntaxTransform * @nogrep */ @@ -13,6 +13,7 @@ export type DeleteDocumentInput = { documentId: string; }; export type DocumentGraphDeleteMutation$variables = { + connections: ReadonlyArray; input: DeleteDocumentInput; }; export type DocumentGraphDeleteMutation$data = { @@ -26,21 +27,24 @@ export type DocumentGraphDeleteMutation = { }; const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" - } -], -v1 = [ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ { "kind": "Variable", "name": "input", "variableName": "input" } ], -v2 = { +v3 = { "alias": null, "args": null, "kind": "ScalarField", @@ -49,20 +53,23 @@ v2 = { }; return { "fragment": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], "kind": "Fragment", "metadata": null, "name": "DocumentGraphDeleteMutation", "selections": [ { "alias": null, - "args": (v1/*: any*/), + "args": (v2/*: any*/), "concreteType": "DeleteDocumentPayload", "kind": "LinkedField", "name": "deleteDocument", "plural": false, "selections": [ - (v2/*: any*/) + (v3/*: any*/) ], "storageKey": null } @@ -72,27 +79,37 @@ return { }, "kind": "Request", "operation": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], "kind": "Operation", "name": "DocumentGraphDeleteMutation", "selections": [ { "alias": null, - "args": (v1/*: any*/), + "args": (v2/*: any*/), "concreteType": "DeleteDocumentPayload", "kind": "LinkedField", "name": "deleteDocument", "plural": false, "selections": [ - (v2/*: any*/), + (v3/*: any*/), { "alias": null, "args": null, "filters": null, - "handle": "deleteRecord", + "handle": "deleteEdge", "key": "", "kind": "ScalarHandle", - "name": "deletedDocumentId" + "name": "deletedDocumentId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] } ], "storageKey": null @@ -110,6 +127,6 @@ return { }; })(); -(node as any).hash = "471020248ed9d398ab80655e04cb823d"; +(node as any).hash = "355f3a70caecabb2146075657665633e"; export default node; diff --git a/apps/console/src/hooks/graph/DocumentGraph.ts b/apps/console/src/hooks/graph/DocumentGraph.ts index 27055c4b1..47668ae00 100644 --- a/apps/console/src/hooks/graph/DocumentGraph.ts +++ b/apps/console/src/hooks/graph/DocumentGraph.ts @@ -11,9 +11,12 @@ import { useMutationWithToasts } from "../useMutationWithToasts"; export const DocumentsConnectionKey = "DocumentsListQuery_documents"; const deleteDocumentMutation = graphql` - mutation DocumentGraphDeleteMutation($input: DeleteDocumentInput!) { + mutation DocumentGraphDeleteMutation( + $input: DeleteDocumentInput! + $connections: [ID!]! + ) { deleteDocument(input: $input) { - deletedDocumentId @deleteRecord + deletedDocumentId @deleteEdge(connections: $connections) } } `; diff --git a/apps/console/src/pages/organizations/documents/DocumentLayout.tsx b/apps/console/src/pages/organizations/documents/DocumentLayout.tsx index 530bb4392..60a85534b 100644 --- a/apps/console/src/pages/organizations/documents/DocumentLayout.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentLayout.tsx @@ -77,8 +77,8 @@ const publishDocumentVersionMutation = graphql` } `; -export function DocumentLayout(props: { queryRef: PreloadedQuery }) { - const { queryRef } = props; +export function DocumentLayout(props: { queryRef: PreloadedQuery; onRefetch: () => void }) { + const { queryRef, onRefetch } = props; const organizationId = useOrganizationId(); const { versionId } = useParams(); @@ -112,6 +112,7 @@ export function DocumentLayout(props: { queryRef: PreloadedQuery )} - + diff --git a/apps/console/src/pages/organizations/documents/DocumentLayoutLoader.tsx b/apps/console/src/pages/organizations/documents/DocumentLayoutLoader.tsx index 1a7468034..8783ba544 100644 --- a/apps/console/src/pages/organizations/documents/DocumentLayoutLoader.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentLayoutLoader.tsx @@ -1,4 +1,4 @@ -import { Suspense, useEffect } from "react"; +import { Suspense, useCallback, useEffect } from "react"; import { useQueryLoader } from "react-relay"; import { useParams } from "react-router"; @@ -25,9 +25,16 @@ function DocumentLayoutQueryLoader() { } }); + const onRefetch = useCallback(() => { + loadQuery( + { documentId, versionId: versionId ?? "", versionSpecified: !!versionId }, + { fetchPolicy: "network-only" }, + ); + }, [documentId, versionId, loadQuery]); + if (!queryRef) return ; - return ; + return ; } export default function DocumentLayoutLoader() { diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx index 3b863d341..4bcd3b9ae 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx @@ -3,14 +3,14 @@ import { useTranslate } from "@probo/i18n"; import { ActionDropdown, DropdownItem, IconArrowDown, IconPencil, IconTrashCan, useConfirm } from "@probo/ui"; import { use, useRef } from "react"; import { useFragment } from "react-relay"; -import { useNavigate } from "react-router"; -import { graphql } from "relay-runtime"; +import { useNavigate, useParams } from "react-router"; +import { ConnectionHandler, graphql } from "relay-runtime"; import type { DocumentActionsDropdown_documentFragment$key } from "#/__generated__/core/DocumentActionsDropdown_documentFragment.graphql"; import type { DocumentActionsDropdown_versionFragment$key } from "#/__generated__/core/DocumentActionsDropdown_versionFragment.graphql"; import type { DocumentActionsDropdownn_exportVersionMutation } from "#/__generated__/core/DocumentActionsDropdownn_exportVersionMutation.graphql"; import { PdfDownloadDialog, type PdfDownloadDialogRef } from "#/components/documents/PdfDownloadDialog"; -import { useDeleteDocumentMutation, useDeleteDraftDocumentVersionMutation } from "#/hooks/graph/DocumentGraph"; +import { DocumentsConnectionKey, useDeleteDocumentMutation, useDeleteDraftDocumentVersionMutation } from "#/hooks/graph/DocumentGraph"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useOrganizationId } from "#/hooks/useOrganizationId"; import { CurrentUser } from "#/providers/CurrentUser"; @@ -53,11 +53,13 @@ const exportDocumentVersionMutation = graphql` export function DocumentActionsDropdownn(props: { documentFragmentRef: DocumentActionsDropdown_documentFragment$key; versionFragmentRef: DocumentActionsDropdown_versionFragment$key; + onRefetch: () => void; }) { - const { documentFragmentRef, versionFragmentRef } = props; + const { documentFragmentRef, versionFragmentRef, onRefetch } = props; const organizationId = useOrganizationId(); const navigate = useNavigate(); + const { versionId } = useParams(); const { __ } = useTranslate(); const { email: defaultEmail } = use(CurrentUser); const updateDialogRef = useRef<{ open: () => void }>(null); @@ -82,11 +84,17 @@ export function DocumentActionsDropdownn(props: { ); const handleDelete = () => { + const connectionId = ConnectionHandler.getConnectionID( + organizationId, + DocumentsConnectionKey, + { orderBy: { direction: "ASC", field: "TITLE" } }, + ); confirm( () => deleteDocument({ variables: { input: { documentId: document.id }, + connections: [connectionId], }, onSuccess() { void navigate(`/organizations/${organizationId}/documents`); @@ -104,15 +112,23 @@ export function DocumentActionsDropdownn(props: { }; const handleDeleteDraft = () => { + const versionsConnectionId = ConnectionHandler.getConnectionID( + document.id, + "DocumentLayout_versions", + ); confirm( () => deleteDraftDocumentVersion({ variables: { input: { documentVersionId: version.id }, - connections: [document.versions.__id], + connections: [document.versions.__id, versionsConnectionId], }, onSuccess() { - window.location.href = `/organizations/${organizationId}/documents/${document.id}`; + if (versionId) { + void navigate(`/organizations/${organizationId}/documents/${document.id}`); + } else { + onRefetch(); + } }, }), { diff --git a/apps/console/src/pages/organizations/documents/_components/UpdateVersionDialog.tsx b/apps/console/src/pages/organizations/documents/_components/UpdateVersionDialog.tsx index 4e500ee90..6dcf7a335 100644 --- a/apps/console/src/pages/organizations/documents/_components/UpdateVersionDialog.tsx +++ b/apps/console/src/pages/organizations/documents/_components/UpdateVersionDialog.tsx @@ -12,6 +12,7 @@ import { } from "@probo/ui"; import { type RefObject, useEffect } from "react"; import { useFragment, useMutation } from "react-relay"; +import { useNavigate } from "react-router"; import { graphql } from "relay-runtime"; import { z } from "zod"; @@ -92,6 +93,7 @@ export default function UpdateVersionDialog(props: UpdateVersionDialogProps) { const { fKey, ref } = props; const organizationId = useOrganizationId(); + const navigate = useNavigate(); const { __ } = useTranslate(); const { toast } = useToast(); const dialogRef = useDialogRef(); @@ -174,11 +176,12 @@ export default function UpdateVersionDialog(props: UpdateVersionDialogProps) { }, onSuccess: () => { dialogRef.current?.close(); - window.location.href = `/organizations/${organizationId}/documents/${document.id}`; + void navigate(`/organizations/${organizationId}/documents/${document.id}/versions/${newVersionId}`); }, }); } else { dialogRef.current?.close(); + void navigate(`/organizations/${organizationId}/documents/${document.id}/versions/${newVersionId}`); } }, }); diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go index 123cd4061..7b71e09d1 100644 --- a/pkg/probo/document_service.go +++ b/pkg/probo/document_service.go @@ -46,6 +46,9 @@ type ( ErrDocumentVersionNoChanges struct { } + ErrDocumentVersionNotDraft struct { + } + ErrDocumentVersionSignatureAlreadySigned struct { } @@ -154,6 +157,10 @@ func (e ErrDocumentVersionNoChanges) Error() string { return "no changes detected" } +func (e ErrDocumentVersionNotDraft) Error() string { + return "cannot update a published document version" +} + func (e ErrDocumentVersionSignatureAlreadySigned) Error() string { return "document version signature already signed" } @@ -781,7 +788,7 @@ func (s *DocumentService) UpdateVersion( } if documentVersion.Status != coredata.DocumentStatusDraft { - return fmt.Errorf("cannot update published version") + return &ErrDocumentVersionNotDraft{} } documentVersion.Title = document.Title diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 9baa53a82..baf520b25 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -4429,8 +4429,16 @@ func (r *mutationResolver) UpdateDocumentVersion(ctx context.Context, input type }, ) if err != nil { - // TODO no panic use gqlutils.InternalError - panic(fmt.Errorf("cannot update document version: %w", err)) + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, gqlutils.NotFound(ctx, err) + } + + if errNotDraft, ok := errors.AsType[*probo.ErrDocumentVersionNotDraft](err); ok { + return nil, gqlutils.Conflict(ctx, errNotDraft) + } + + r.logger.ErrorCtx(ctx, "cannot update document version", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return &types.UpdateDocumentVersionPayload{