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 <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-10 16:10:23 +01:00
parent 4fbb5acb70
commit c493e31745
8 changed files with 98 additions and 36 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<6b7916e6ff46a629bf97a7b072db2d8b>> * @generated SignedSource<<64c2c92a2a0806f34243f8fc9904167c>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -13,6 +13,7 @@ export type DeleteDocumentInput = {
documentId: string; documentId: string;
}; };
export type DocumentGraphDeleteMutation$variables = { export type DocumentGraphDeleteMutation$variables = {
connections: ReadonlyArray<string>;
input: DeleteDocumentInput; input: DeleteDocumentInput;
}; };
export type DocumentGraphDeleteMutation$data = { export type DocumentGraphDeleteMutation$data = {
@@ -26,21 +27,24 @@ export type DocumentGraphDeleteMutation = {
}; };
const node: ConcreteRequest = (function(){ const node: ConcreteRequest = (function(){
var v0 = [ var v0 = {
{ "defaultValue": null,
"defaultValue": null, "kind": "LocalArgument",
"kind": "LocalArgument", "name": "connections"
"name": "input" },
} v1 = {
], "defaultValue": null,
v1 = [ "kind": "LocalArgument",
"name": "input"
},
v2 = [
{ {
"kind": "Variable", "kind": "Variable",
"name": "input", "name": "input",
"variableName": "input" "variableName": "input"
} }
], ],
v2 = { v3 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
@@ -49,20 +53,23 @@ v2 = {
}; };
return { return {
"fragment": { "fragment": {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "DocumentGraphDeleteMutation", "name": "DocumentGraphDeleteMutation",
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
"args": (v1/*: any*/), "args": (v2/*: any*/),
"concreteType": "DeleteDocumentPayload", "concreteType": "DeleteDocumentPayload",
"kind": "LinkedField", "kind": "LinkedField",
"name": "deleteDocument", "name": "deleteDocument",
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/) (v3/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -72,27 +79,37 @@ return {
}, },
"kind": "Request", "kind": "Request",
"operation": { "operation": {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation", "kind": "Operation",
"name": "DocumentGraphDeleteMutation", "name": "DocumentGraphDeleteMutation",
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
"args": (v1/*: any*/), "args": (v2/*: any*/),
"concreteType": "DeleteDocumentPayload", "concreteType": "DeleteDocumentPayload",
"kind": "LinkedField", "kind": "LinkedField",
"name": "deleteDocument", "name": "deleteDocument",
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/), (v3/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"filters": null, "filters": null,
"handle": "deleteRecord", "handle": "deleteEdge",
"key": "", "key": "",
"kind": "ScalarHandle", "kind": "ScalarHandle",
"name": "deletedDocumentId" "name": "deletedDocumentId",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
} }
], ],
"storageKey": null "storageKey": null
@@ -110,6 +127,6 @@ return {
}; };
})(); })();
(node as any).hash = "471020248ed9d398ab80655e04cb823d"; (node as any).hash = "355f3a70caecabb2146075657665633e";
export default node; export default node;

View File

@@ -11,9 +11,12 @@ import { useMutationWithToasts } from "../useMutationWithToasts";
export const DocumentsConnectionKey = "DocumentsListQuery_documents"; export const DocumentsConnectionKey = "DocumentsListQuery_documents";
const deleteDocumentMutation = graphql` const deleteDocumentMutation = graphql`
mutation DocumentGraphDeleteMutation($input: DeleteDocumentInput!) { mutation DocumentGraphDeleteMutation(
$input: DeleteDocumentInput!
$connections: [ID!]!
) {
deleteDocument(input: $input) { deleteDocument(input: $input) {
deletedDocumentId @deleteRecord deletedDocumentId @deleteEdge(connections: $connections)
} }
} }
`; `;

View File

@@ -77,8 +77,8 @@ const publishDocumentVersionMutation = graphql`
} }
`; `;
export function DocumentLayout(props: { queryRef: PreloadedQuery<DocumentLayoutQuery> }) { export function DocumentLayout(props: { queryRef: PreloadedQuery<DocumentLayoutQuery>; onRefetch: () => void }) {
const { queryRef } = props; const { queryRef, onRefetch } = props;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { versionId } = useParams(); const { versionId } = useParams();
@@ -112,6 +112,7 @@ export function DocumentLayout(props: { queryRef: PreloadedQuery<DocumentLayoutQ
variables: { variables: {
input: { documentId: document.id }, input: { documentId: document.id },
}, },
onSuccess: onRefetch,
}); });
}; };
@@ -146,7 +147,7 @@ export function DocumentLayout(props: { queryRef: PreloadedQuery<DocumentLayoutQ
</Button> </Button>
)} )}
<DocumentVersionsDropdown /> <DocumentVersionsDropdown />
<DocumentActionsDropdownn documentFragmentRef={document} versionFragmentRef={currentVersion} /> <DocumentActionsDropdownn documentFragmentRef={document} versionFragmentRef={currentVersion} onRefetch={onRefetch} />
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Suspense, useEffect } from "react"; import { Suspense, useCallback, useEffect } from "react";
import { useQueryLoader } from "react-relay"; import { useQueryLoader } from "react-relay";
import { useParams } from "react-router"; 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 <PageSkeleton />; if (!queryRef) return <PageSkeleton />;
return <DocumentLayout queryRef={queryRef} />; return <DocumentLayout queryRef={queryRef} onRefetch={onRefetch} />;
} }
export default function DocumentLayoutLoader() { export default function DocumentLayoutLoader() {

View File

@@ -3,14 +3,14 @@ import { useTranslate } from "@probo/i18n";
import { ActionDropdown, DropdownItem, IconArrowDown, IconPencil, IconTrashCan, useConfirm } from "@probo/ui"; import { ActionDropdown, DropdownItem, IconArrowDown, IconPencil, IconTrashCan, useConfirm } from "@probo/ui";
import { use, useRef } from "react"; import { use, useRef } from "react";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useNavigate } from "react-router"; import { useNavigate, useParams } from "react-router";
import { graphql } from "relay-runtime"; import { ConnectionHandler, graphql } from "relay-runtime";
import type { DocumentActionsDropdown_documentFragment$key } from "#/__generated__/core/DocumentActionsDropdown_documentFragment.graphql"; import type { DocumentActionsDropdown_documentFragment$key } from "#/__generated__/core/DocumentActionsDropdown_documentFragment.graphql";
import type { DocumentActionsDropdown_versionFragment$key } from "#/__generated__/core/DocumentActionsDropdown_versionFragment.graphql"; import type { DocumentActionsDropdown_versionFragment$key } from "#/__generated__/core/DocumentActionsDropdown_versionFragment.graphql";
import type { DocumentActionsDropdownn_exportVersionMutation } from "#/__generated__/core/DocumentActionsDropdownn_exportVersionMutation.graphql"; import type { DocumentActionsDropdownn_exportVersionMutation } from "#/__generated__/core/DocumentActionsDropdownn_exportVersionMutation.graphql";
import { PdfDownloadDialog, type PdfDownloadDialogRef } from "#/components/documents/PdfDownloadDialog"; 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 { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CurrentUser } from "#/providers/CurrentUser"; import { CurrentUser } from "#/providers/CurrentUser";
@@ -53,11 +53,13 @@ const exportDocumentVersionMutation = graphql`
export function DocumentActionsDropdownn(props: { export function DocumentActionsDropdownn(props: {
documentFragmentRef: DocumentActionsDropdown_documentFragment$key; documentFragmentRef: DocumentActionsDropdown_documentFragment$key;
versionFragmentRef: DocumentActionsDropdown_versionFragment$key; versionFragmentRef: DocumentActionsDropdown_versionFragment$key;
onRefetch: () => void;
}) { }) {
const { documentFragmentRef, versionFragmentRef } = props; const { documentFragmentRef, versionFragmentRef, onRefetch } = props;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const navigate = useNavigate(); const navigate = useNavigate();
const { versionId } = useParams();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { email: defaultEmail } = use(CurrentUser); const { email: defaultEmail } = use(CurrentUser);
const updateDialogRef = useRef<{ open: () => void }>(null); const updateDialogRef = useRef<{ open: () => void }>(null);
@@ -82,11 +84,17 @@ export function DocumentActionsDropdownn(props: {
); );
const handleDelete = () => { const handleDelete = () => {
const connectionId = ConnectionHandler.getConnectionID(
organizationId,
DocumentsConnectionKey,
{ orderBy: { direction: "ASC", field: "TITLE" } },
);
confirm( confirm(
() => () =>
deleteDocument({ deleteDocument({
variables: { variables: {
input: { documentId: document.id }, input: { documentId: document.id },
connections: [connectionId],
}, },
onSuccess() { onSuccess() {
void navigate(`/organizations/${organizationId}/documents`); void navigate(`/organizations/${organizationId}/documents`);
@@ -104,15 +112,23 @@ export function DocumentActionsDropdownn(props: {
}; };
const handleDeleteDraft = () => { const handleDeleteDraft = () => {
const versionsConnectionId = ConnectionHandler.getConnectionID(
document.id,
"DocumentLayout_versions",
);
confirm( confirm(
() => () =>
deleteDraftDocumentVersion({ deleteDraftDocumentVersion({
variables: { variables: {
input: { documentVersionId: version.id }, input: { documentVersionId: version.id },
connections: [document.versions.__id], connections: [document.versions.__id, versionsConnectionId],
}, },
onSuccess() { onSuccess() {
window.location.href = `/organizations/${organizationId}/documents/${document.id}`; if (versionId) {
void navigate(`/organizations/${organizationId}/documents/${document.id}`);
} else {
onRefetch();
}
}, },
}), }),
{ {

View File

@@ -12,6 +12,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { type RefObject, useEffect } from "react"; import { type RefObject, useEffect } from "react";
import { useFragment, useMutation } from "react-relay"; import { useFragment, useMutation } from "react-relay";
import { useNavigate } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { z } from "zod"; import { z } from "zod";
@@ -92,6 +93,7 @@ export default function UpdateVersionDialog(props: UpdateVersionDialogProps) {
const { fKey, ref } = props; const { fKey, ref } = props;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const navigate = useNavigate();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
@@ -174,11 +176,12 @@ export default function UpdateVersionDialog(props: UpdateVersionDialogProps) {
}, },
onSuccess: () => { onSuccess: () => {
dialogRef.current?.close(); dialogRef.current?.close();
window.location.href = `/organizations/${organizationId}/documents/${document.id}`; void navigate(`/organizations/${organizationId}/documents/${document.id}/versions/${newVersionId}`);
}, },
}); });
} else { } else {
dialogRef.current?.close(); dialogRef.current?.close();
void navigate(`/organizations/${organizationId}/documents/${document.id}/versions/${newVersionId}`);
} }
}, },
}); });

View File

@@ -46,6 +46,9 @@ type (
ErrDocumentVersionNoChanges struct { ErrDocumentVersionNoChanges struct {
} }
ErrDocumentVersionNotDraft struct {
}
ErrDocumentVersionSignatureAlreadySigned struct { ErrDocumentVersionSignatureAlreadySigned struct {
} }
@@ -154,6 +157,10 @@ func (e ErrDocumentVersionNoChanges) Error() string {
return "no changes detected" return "no changes detected"
} }
func (e ErrDocumentVersionNotDraft) Error() string {
return "cannot update a published document version"
}
func (e ErrDocumentVersionSignatureAlreadySigned) Error() string { func (e ErrDocumentVersionSignatureAlreadySigned) Error() string {
return "document version signature already signed" return "document version signature already signed"
} }
@@ -781,7 +788,7 @@ func (s *DocumentService) UpdateVersion(
} }
if documentVersion.Status != coredata.DocumentStatusDraft { if documentVersion.Status != coredata.DocumentStatusDraft {
return fmt.Errorf("cannot update published version") return &ErrDocumentVersionNotDraft{}
} }
documentVersion.Title = document.Title documentVersion.Title = document.Title

View File

@@ -4429,8 +4429,16 @@ func (r *mutationResolver) UpdateDocumentVersion(ctx context.Context, input type
}, },
) )
if err != nil { if err != nil {
// TODO no panic use gqlutils.InternalError if errors.Is(err, coredata.ErrResourceNotFound) {
panic(fmt.Errorf("cannot update document version: %w", err)) 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{ return &types.UpdateDocumentVersionPayload{