Better handling of document versions

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-08-08 15:12:06 +02:00
parent 81e7cd3574
commit a43328560a
12 changed files with 621 additions and 83 deletions

View File

@@ -3,6 +3,7 @@ import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "../useMutationWithToasts";
import type { DocumentGraphDeleteMutation } from "./__generated__/DocumentGraphDeleteMutation.graphql";
import type { DocumentGraphSendSigningNotificationsMutation } from "./__generated__/DocumentGraphSendSigningNotificationsMutation.graphql";
import type { DocumentGraphDeleteDraftMutation } from "./__generated__/DocumentGraphDeleteDraftMutation.graphql";
export const documentsQuery = graphql`
query DocumentGraphListQuery($organizationId: ID!) {
@@ -13,15 +14,14 @@ export const documentsQuery = graphql`
}
`;
export const DocumentsConnectionKey = "DocumentsPageFragment_documents";
export const DocumentsConnectionKey = "DocumentsListQuery_documents";
const deleteDocumentMutation = graphql`
mutation DocumentGraphDeleteMutation(
$input: DeleteDocumentInput!
$connections: [ID!]!
) {
deleteDocument(input: $input) {
deletedDocumentId @deleteEdge(connections: $connections)
deletedDocumentId @deleteRecord
}
}
`;
@@ -38,6 +38,29 @@ export function useDeleteDocumentMutation() {
);
}
const deleteDraftDocumentVersionMutation = graphql`
mutation DocumentGraphDeleteDraftMutation(
$input: DeleteDraftDocumentVersionInput!
$connections: [ID!]!
) {
deleteDraftDocumentVersion(input: $input) {
deletedDocumentVersionId @deleteEdge(connections: $connections)
}
}
`;
export function useDeleteDraftDocumentVersionMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<DocumentGraphDeleteDraftMutation>(
deleteDraftDocumentVersionMutation,
{
successMessage: __("Draft deleted successfully."),
errorMessage: __("Failed to delete draft. Please try again."),
}
);
}
const sendSigningNotificationsMutation = graphql`
mutation DocumentGraphSendSigningNotificationsMutation(
$input: SendSigningNotificationsInput!

View File

@@ -0,0 +1,132 @@
/**
* @generated SignedSource<<f114dd31b601c0097380450ab5029b01>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteDraftDocumentVersionInput = {
documentVersionId: string;
};
export type DocumentGraphDeleteDraftMutation$variables = {
connections: ReadonlyArray<string>;
input: DeleteDraftDocumentVersionInput;
};
export type DocumentGraphDeleteDraftMutation$data = {
readonly deleteDraftDocumentVersion: {
readonly deletedDocumentVersionId: string;
};
};
export type DocumentGraphDeleteDraftMutation = {
response: DocumentGraphDeleteDraftMutation$data;
variables: DocumentGraphDeleteDraftMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deletedDocumentVersionId",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "DocumentGraphDeleteDraftMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "DeleteDraftDocumentVersionPayload",
"kind": "LinkedField",
"name": "deleteDraftDocumentVersion",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "DocumentGraphDeleteDraftMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "DeleteDraftDocumentVersionPayload",
"kind": "LinkedField",
"name": "deleteDraftDocumentVersion",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "deleteEdge",
"key": "",
"kind": "ScalarHandle",
"name": "deletedDocumentVersionId",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "cd7e8253362e1d7e719e4ae2c206756e",
"id": null,
"metadata": {},
"name": "DocumentGraphDeleteDraftMutation",
"operationKind": "mutation",
"text": "mutation DocumentGraphDeleteDraftMutation(\n $input: DeleteDraftDocumentVersionInput!\n) {\n deleteDraftDocumentVersion(input: $input) {\n deletedDocumentVersionId\n }\n}\n"
}
};
})();
(node as any).hash = "d43940b41e3838fd03c3341c13b4c5c5";
export default node;

View File

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

View File

@@ -1,6 +1,5 @@
import type { PreloadedQuery } from "react-relay";
import {
ConnectionHandler,
graphql,
loadQuery,
useFragment,
@@ -9,8 +8,8 @@ import {
import type { DocumentGraphNodeQuery } from "/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql";
import {
documentNodeQuery,
DocumentsConnectionKey,
useDeleteDocumentMutation,
useDeleteDraftDocumentVersionMutation,
} from "/hooks/graph/DocumentGraph";
import { usePageTitle } from "@probo/hooks";
import type {
@@ -187,6 +186,7 @@ export default function DocumentDetailPage(props: Props) {
}
);
const [deleteDocument, isDeleting] = useDeleteDocumentMutation();
const [deleteDraftDocumentVersion, isDeletingDraft] = useDeleteDraftDocumentVersionMutation();
const [exportDocumentVersionPDF, isExporting] =
useMutationWithToasts<DocumentDetailPageExportPDFMutation>(
exportDocumentVersionPDFMutation,
@@ -285,17 +285,13 @@ export default function DocumentDetailPage(props: Props) {
confirm(
() =>
new Promise<void>((resolve) => {
const connectionId = ConnectionHandler.getConnectionID(
organizationId,
DocumentsConnectionKey
);
deleteDocument({
variables: {
input: { documentId: document.id },
connections: [connectionId],
},
onSuccess() {
navigate(`/organizations/${organizationId}/documents`);
resolve();
},
onError: () => resolve(),
});
@@ -311,6 +307,40 @@ export default function DocumentDetailPage(props: Props) {
);
};
const handleDeleteDraft = () => {
confirm(
() =>
new Promise<void>((resolve) => {
deleteDraftDocumentVersion({
variables: {
input: { documentVersionId: currentVersion.id },
connections: [versionConnectionId],
},
onSuccess() {
loadQuery(
props.queryRef.environment,
documentNodeQuery,
props.queryRef.variables,
{ fetchPolicy: "network-only" }
);
resolve();
},
onError: () => resolve(),
});
}),
{
message: sprintf(
__(
'This will permanently delete the draft version %s of "%s". This action cannot be undone.'
),
currentVersion.version,
document.title
),
}
);
};
const handleDownloadPdf = () => {
exportDocumentVersionPDF({
variables: {
@@ -392,6 +422,15 @@ export default function DocumentDetailPage(props: Props) {
>
{isDraft ? __("Edit draft document") : __("Create new draft")}
</DropdownItem>
{isDraft && versions.length > 1 && (
<DropdownItem
onClick={handleDeleteDraft}
icon={IconTrashCan}
disabled={isDeletingDraft}
>
{__("Delete draft document")}
</DropdownItem>
)}
<DropdownItem
onClick={handleDownloadPdf}
icon={IconArrowDown}
@@ -405,7 +444,7 @@ export default function DocumentDetailPage(props: Props) {
disabled={isDeleting}
onClick={handleDelete}
>
{__("Delete")}
{__("Delete document")}
</DropdownItem>
</ActionDropdown>
</div>

View File

@@ -227,7 +227,6 @@ const rowFragment = graphql`
function DocumentRow({
document: documentKey,
organizationId,
connectionId,
checked,
onCheck,
}: {
@@ -257,7 +256,6 @@ function DocumentRow({
deleteDocument({
variables: {
input: { documentId: document.id },
connections: [connectionId],
},
}),
{

View File

@@ -101,28 +101,6 @@ export default function UpdateVersionDialog({
ref.current = {
open: () => {
dialogRef.current?.open();
if (!isDraft) {
createDraftDocumentVersion({
variables: {
input: {
documentID: document.id,
},
connections: [connectionId],
},
onCompleted: (_, errors) => {
if (errors) {
toast({
variant: "error",
title: __("Error creating draft"),
description:
errors[0]?.message || __("An unknown error occurred"),
});
dialogRef.current?.close();
return;
}
},
});
}
},
};
@@ -131,25 +109,65 @@ export default function UpdateVersionDialog({
}
const onSubmit = handleSubmit((data) => {
updateDocumentVersion({
variables: {
input: {
documentVersionId: version.id,
content: data.content,
if (isDraft) {
updateDocumentVersion({
variables: {
input: {
documentVersionId: version.id,
content: data.content,
},
},
},
onSuccess: () => {
dialogRef.current?.close();
},
});
onSuccess: () => {
dialogRef.current?.close();
},
});
} else {
createDraftDocumentVersion({
variables: {
input: {
documentID: document.id,
},
connections: [connectionId],
},
onCompleted: (createResponse, errors) => {
if (errors) {
toast({
variant: "error",
title: __("Error creating draft"),
description:
errors[0]?.message || __("An unknown error occurred"),
});
return;
}
const newVersionId = createResponse?.createDraftDocumentVersion?.documentVersionEdge?.node?.id;
if (newVersionId && data.content !== version.content) {
updateDocumentVersion({
variables: {
input: {
documentVersionId: newVersionId,
content: data.content,
},
},
onSuccess: () => {
dialogRef.current?.close();
},
});
} else {
dialogRef.current?.close();
}
},
});
}
});
const isLoading = isCreatingDraft || isUpdating;
return (
<Dialog
ref={dialogRef}
title={<Breadcrumb items={[__("Documents"), __("Edit document")]} />}
>
{isCreatingDraft && <Spinner centered />}
<form onSubmit={onSubmit}>
<DialogContent>
<Textarea
@@ -164,7 +182,8 @@ export default function UpdateVersionDialog({
/>
</DialogContent>
<DialogFooter>
<Button disabled={isUpdating} type="submit">
<Button disabled={isLoading} type="submit">
{isLoading && <Spinner />}
{__("Update document")}
</Button>
</DialogFooter>