From 4084554daaaa51076c8399e4cee050b96ec84eaa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 30 May 2026 00:48:33 +0000 Subject: [PATCH] Add document archive row action Allow documents to be archived or unarchived directly from the list row actions, matching the detail-page behavior. Remove the row from the active or archived connection after the status change so filtered lists update immediately. Signed-off-by: Cursor Agent Co-authored-by: Bryan FRIMIN --- .../documents/_components/DocumentList.tsx | 2 +- .../_components/DocumentListItem.tsx | 187 ++++++++++++++++-- 2 files changed, 174 insertions(+), 15 deletions(-) diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx index df5ba7b82..4ea4156a8 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx @@ -173,7 +173,7 @@ export function DocumentList(props: { const canSendAnySignatureNotifications = documents.some( ({ canSendSigningNotifications }) => canSendSigningNotifications, ); - const hasAnyAction = tab === "ARCHIVED" ? canUnarchiveAny || canDeleteAny : canDeleteAny || canUpdateAny; + const hasAnyAction = tab === "ARCHIVED" ? canUnarchiveAny || canDeleteAny : canArchiveAny || canDeleteAny || canUpdateAny; useEffect(() => { onConnectionIdChange(connectionId); diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentListItem.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentListItem.tsx index f87552d40..7c1cb201d 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentListItem.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentListItem.tsx @@ -12,21 +12,43 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. -import { formatDate, getDocumentClassificationLabel, getDocumentTypeLabel, sprintf } from "@probo/helpers"; +import { + formatDate, + formatError, + getDocumentClassificationLabel, + getDocumentTypeLabel, + sprintf, +} from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; -import { ActionDropdown, Badge, Checkbox, DropdownItem, IconTrashCan, Td, Tr, useConfirm } from "@probo/ui"; +import { + ActionDropdown, + Badge, + Checkbox, + DropdownItem, + IconArchive, + IconTrashCan, + Td, + Tr, + useConfirm, + useToast, +} from "@probo/ui"; import { useFragment, useMutation } from "react-relay"; -import { type DataID, graphql } from "relay-runtime"; +import { ConnectionHandler, type DataID, graphql } from "relay-runtime"; +import type { DocumentListItem_archiveMutation } from "#/__generated__/core/DocumentListItem_archiveMutation.graphql"; import type { DocumentListItem_deleteMutation } from "#/__generated__/core/DocumentListItem_deleteMutation.graphql"; +import type { DocumentListItem_unarchiveMutation } from "#/__generated__/core/DocumentListItem_unarchiveMutation.graphql"; import type { DocumentListItemFragment$key } from "#/__generated__/core/DocumentListItemFragment.graphql"; import { useOrganizationId } from "#/hooks/useOrganizationId"; const fragment = graphql` fragment DocumentListItemFragment on Document { id + status updatedAt + canArchive: permission(action: "core:document:archive") canDelete: permission(action: "core:document:delete") + canUnarchive: permission(action: "core:document:unarchive") defaultApprovers { id fullName @@ -66,6 +88,23 @@ const fragment = graphql` } `; +const archiveDocumentMutation = graphql` + mutation DocumentListItem_archiveMutation( + $input: ArchiveDocumentInput! + ) { + archiveDocument(input: $input) { + document { + id + status + archivedAt + canArchive: permission(action: "core:document:archive") + canUnarchive: permission(action: "core:document:unarchive") + canDelete: permission(action: "core:document:delete") + } + } + } +`; + const deleteDocumentMutation = graphql` mutation DocumentListItem_deleteMutation( $input: DeleteDocumentInput! @@ -77,6 +116,23 @@ const deleteDocumentMutation = graphql` } `; +const unarchiveDocumentMutation = graphql` + mutation DocumentListItem_unarchiveMutation( + $input: UnarchiveDocumentInput! + ) { + unarchiveDocument(input: $input) { + document { + id + status + archivedAt + canArchive: permission(action: "core:document:archive") + canUnarchive: permission(action: "core:document:unarchive") + canDelete: permission(action: "core:document:delete") + } + } + } +`; + export function DocumentListItem(props: { fragmentRef: DocumentListItemFragment$key; connectionId: DataID; @@ -94,7 +150,10 @@ export function DocumentListItem(props: { const organizationId = useOrganizationId(); const { __ } = useTranslate(); + const { toast } = useToast(); + const [archiveDocument, isArchiving] = useMutation(archiveDocumentMutation); const [deleteDocument] = useMutation(deleteDocumentMutation); + const [unarchiveDocument, isUnarchiving] = useMutation(unarchiveDocumentMutation); const confirm = useConfirm(); const document = useFragment( fragment, @@ -117,6 +176,51 @@ export function DocumentListItem(props: { PUBLISHED: __("Published"), } as const; + const handleArchive = () => { + confirm( + () => + new Promise((resolve) => { + archiveDocument({ + variables: { input: { documentId: document.id } }, + updater(store) { + const conn = store.get(connectionId); + if (conn) { + ConnectionHandler.deleteNode(conn, document.id); + } + }, + onCompleted(_, errors) { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError(__("Failed to archive document"), errors), + variant: "error", + }); + } else { + toast({ + title: __("Success"), + description: __("Document archived successfully."), + variant: "success", + }); + } + resolve(); + }, + onError(error) { + toast({ title: __("Error"), description: error.message, variant: "error" }); + resolve(); + }, + }); + }), + { + message: sprintf( + __("This will archive the document \"%s\". It will no longer be editable."), + lastVersion.title, + ), + variant: "danger", + label: __("Archive"), + }, + ); + }; + const handleDelete = () => { confirm( () => @@ -141,6 +245,41 @@ export function DocumentListItem(props: { ); }; + const handleUnarchive = () => { + unarchiveDocument({ + variables: { input: { documentId: document.id } }, + updater(store) { + const conn = store.get(connectionId); + if (conn) { + ConnectionHandler.deleteNode(conn, document.id); + } + }, + onCompleted(_, errors) { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError(__("Failed to unarchive document"), errors), + variant: "error", + }); + return; + } + toast({ + title: __("Success"), + description: __("Document unarchived successfully."), + variant: "success", + }); + }, + onError(error) { + toast({ title: __("Error"), description: error.message, variant: "error" }); + }, + }); + }; + + const hasRowAction + = (document.canArchive && document.status === "ACTIVE") + || (document.canUnarchive && document.status === "ARCHIVED") + || document.canDelete; + return ( {hasAnyAction && ( - - {document.canDelete && ( - - {__("Delete")} - - )} - + {hasRowAction && ( + + {document.canArchive && document.status === "ACTIVE" && ( + + {__("Archive")} + + )} + {document.canUnarchive && document.status === "ARCHIVED" && ( + + {__("Unarchive")} + + )} + {document.canDelete && ( + + {__("Delete")} + + )} + + )} )}