From c195320649c6520c6077a1d524e23b42c6ed6386 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 16 Jun 2026 13:35:48 +0000 Subject: [PATCH] Add document delete confirmation Route document deletion through explicit confirmation dialogs so single and bulk delete actions require users to acknowledge the destructive operation before the mutation runs. Signed-off-by: Cursor Agent Co-authored-by: Sacha Al Himdani --- .../_components/DeleteDocumentDialog.tsx | 177 +++++++++++++++ .../_components/DocumentActionsDropdown.tsx | 48 ++-- .../documents/_components/DocumentList.tsx | 37 +--- .../_components/DocumentListItem.tsx | 209 ++++++++---------- 4 files changed, 299 insertions(+), 172 deletions(-) create mode 100644 apps/console/src/pages/organizations/documents/_components/DeleteDocumentDialog.tsx diff --git a/apps/console/src/pages/organizations/documents/_components/DeleteDocumentDialog.tsx b/apps/console/src/pages/organizations/documents/_components/DeleteDocumentDialog.tsx new file mode 100644 index 000000000..9128c18b6 --- /dev/null +++ b/apps/console/src/pages/organizations/documents/_components/DeleteDocumentDialog.tsx @@ -0,0 +1,177 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { sprintf } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Spinner, + useDialogRef, +} from "@probo/ui"; +import { forwardRef, useImperativeHandle } from "react"; +import { ConnectionHandler, type DataID } from "relay-runtime"; + +import { + useBulkDeleteDocumentsMutation, + useDeleteDocumentMutation, +} from "#/hooks/graph/DocumentGraph"; + +export type DeleteDocumentDialogRef = { + open: () => void; + close: () => void; +}; + +type DeleteDocumentDialogProps = { + documentId: string; + documentTitle: string; + connections: DataID[]; + onSuccess?: () => void; +}; + +export const DeleteDocumentDialog = forwardRef< + DeleteDocumentDialogRef, + DeleteDocumentDialogProps +>(({ documentId, documentTitle, connections, onSuccess }, ref) => { + const { __ } = useTranslate(); + const dialogRef = useDialogRef(); + const [deleteDocument, isDeleting] = useDeleteDocumentMutation(); + + useImperativeHandle(ref, () => ({ + open: () => dialogRef.current?.open(), + close: () => dialogRef.current?.close(), + })); + + const handleDelete = async () => { + try { + await deleteDocument({ + variables: { + input: { documentId }, + connections, + }, + }); + dialogRef.current?.close(); + onSuccess?.(); + } catch { + // The mutation helper already displays the error toast. + } + }; + + return ( + + +

+ {sprintf( + __("Are you sure you want to delete the document \"%s\"?"), + documentTitle, + )} +

+

+ {__("This action cannot be undone.")} +

+
+ + + +
+ ); +}); + +DeleteDocumentDialog.displayName = "DeleteDocumentDialog"; + +type DeleteDocumentsDialogProps = { + documentIds: string[]; + connectionId: DataID; + onSuccess?: () => void; +}; + +export const DeleteDocumentsDialog = forwardRef< + DeleteDocumentDialogRef, + DeleteDocumentsDialogProps +>(({ documentIds, connectionId, onSuccess }, ref) => { + const { __ } = useTranslate(); + const dialogRef = useDialogRef(); + const [deleteDocuments, isDeleting] = useBulkDeleteDocumentsMutation(); + const documentCount = documentIds.length; + + useImperativeHandle(ref, () => ({ + open: () => dialogRef.current?.open(), + close: () => dialogRef.current?.close(), + })); + + const handleDelete = async () => { + try { + await deleteDocuments({ + variables: { input: { documentIds } }, + updater: (store) => { + const conn = store.get(connectionId); + if (conn) { + documentIds.forEach(id => ConnectionHandler.deleteNode(conn, id)); + } + }, + }); + dialogRef.current?.close(); + onSuccess?.(); + } catch { + // The mutation helper already displays the error toast. + } + }; + + return ( + + +

+ {documentCount === 1 + ? __("Are you sure you want to delete 1 selected document?") + : sprintf( + __("Are you sure you want to delete %s selected documents?"), + documentCount, + )} +

+

+ {__("This action cannot be undone.")} +

+
+ + + +
+ ); +}); + +DeleteDocumentsDialog.displayName = "DeleteDocumentsDialog"; diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx index 8dc2e351a..045a4ae21 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentActionsDropdown.tsx @@ -27,10 +27,12 @@ import type { DocumentActionsDropdown_exportVersionMutation } from "#/__generate import type { DocumentActionsDropdown_unarchiveMutation } from "#/__generated__/core/DocumentActionsDropdown_unarchiveMutation.graphql"; import type { DocumentActionsDropdown_versionFragment$key } from "#/__generated__/core/DocumentActionsDropdown_versionFragment.graphql"; import { PdfDownloadDialog, type PdfDownloadDialogRef } from "#/components/documents/PdfDownloadDialog"; -import { DocumentsConnectionKey, useDeleteDocumentMutation } from "#/hooks/graph/DocumentGraph"; +import { DocumentsConnectionKey } from "#/hooks/graph/DocumentGraph"; import { useOrganizationId } from "#/hooks/useOrganizationId"; import { CurrentUser } from "#/providers/CurrentUser"; +import { DeleteDocumentDialog, type DeleteDocumentDialogRef } from "./DeleteDocumentDialog"; + const documentFragment = graphql` fragment DocumentActionsDropdown_documentFragment on Document { id @@ -121,13 +123,13 @@ export function DocumentActionsDropdown(props: { const { __ } = useTranslate(); const { email: defaultEmail } = use(CurrentUser); const pdfDownloadDialogRef = useRef(null); + const deleteDocumentDialogRef = useRef(null); const confirm = useConfirm(); const { toast } = useToast(); const document = useFragment(documentFragment, documentFragmentRef); const version = useFragment(versionFragment, versionFragmentRef); - const [deleteDocument, isDeleting] = useDeleteDocumentMutation(); const [archiveDocument, isArchiving] = useMutation(archiveDocumentMutation); const [unarchiveDocument, isUnarchiving] @@ -214,33 +216,11 @@ export function DocumentActionsDropdown(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`); - }, - }), - { - message: sprintf( - __( - "This will permanently delete the document \"%s\". This action cannot be undone.", - ), - version.title, - ), - }, - ); - }; + const documentsConnectionId = ConnectionHandler.getConnectionID( + organizationId, + DocumentsConnectionKey, + { orderBy: { direction: "ASC", field: "TITLE" } }, + ); const handleExportDocumentVersion = (options: { withWatermark: boolean; @@ -290,6 +270,13 @@ export function DocumentActionsDropdown(props: { isLoading={isExporting} defaultEmail={defaultEmail} /> + void navigate(`/organizations/${organizationId}/documents`)} + /> pdfDownloadDialogRef.current?.open()} @@ -329,8 +316,7 @@ export function DocumentActionsDropdown(props: { deleteDocumentDialogRef.current?.open()} > {__("Delete document")} diff --git a/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx b/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx index f0c4fedf9..777daab44 100644 --- a/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx +++ b/apps/console/src/pages/organizations/documents/_components/DocumentList.tsx @@ -15,7 +15,7 @@ import { documentClassifications, documentTypes, documentWriteModes, getDocumentClassificationLabel, getDocumentTypeLabel, getDocumentWriteModeLabel, sprintf } from "@probo/helpers"; import { useList } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; -import { Button, Card, Checkbox, IconArchive, IconArrowDown, IconCrossLargeX, IconSignature, IconTrashCan, IconUpload, Option, Select, Tbody, Th, Thead, Tr, useConfirm } from "@probo/ui"; +import { Button, Card, Checkbox, IconArchive, IconArrowDown, IconCrossLargeX, IconSignature, IconTrashCan, IconUpload, Option, Select, Tbody, Th, Thead, Tr } from "@probo/ui"; import { type ComponentProps, use, useEffect, useRef, useState, useTransition } from "react"; import { usePaginationFragment } from "react-relay"; import { ConnectionHandler, graphql } from "relay-runtime"; @@ -26,11 +26,12 @@ import type { DocumentListFragment$key } from "#/__generated__/core/DocumentList import type { DocumentClassification, DocumentOrderField, DocumentsListQuery, DocumentType, DocumentWriteMode } from "#/__generated__/core/DocumentsListQuery.graphql"; import { BulkExportDialog, type BulkExportDialogRef } from "#/components/documents/BulkExportDialog"; import { type Order, SortableTable, SortableTh } from "#/components/SortableTable"; -import { useBulkDeleteDocumentsMutation, useBulkExportDocumentsMutation } from "#/hooks/graph/DocumentGraph"; +import { useBulkExportDocumentsMutation } from "#/hooks/graph/DocumentGraph"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useOrganizationId } from "#/hooks/useOrganizationId"; import { CurrentUser } from "#/providers/CurrentUser"; +import { type DeleteDocumentDialogRef, DeleteDocumentsDialog } from "./DeleteDocumentDialog"; import { DocumentListItem } from "./DocumentListItem"; import { PublishDocumentsDialog } from "./PublishDocumentsDialog"; import { SignatureDocumentsDialog } from "./SignatureDocumentsDialog"; @@ -122,6 +123,7 @@ export function DocumentList(props: { const organizationId = useOrganizationId(); const { email: defaultEmail } = use(CurrentUser); const bulkExportDialogRef = useRef(null); + const deleteDocumentsDialogRef = useRef(null); const { __ } = useTranslate(); const pagination = usePaginationFragment( @@ -152,7 +154,6 @@ export function DocumentList(props: { const documents = pagination.data.documents.edges.map(({ node }) => node); const connectionId = pagination.data.documents.__id; - const [bulkDeleteDocuments] = useBulkDeleteDocumentsMutation(); const [bulkExportDocuments, isBulkExporting] = useBulkExportDocumentsMutation(); const [bulkArchiveDocuments, isBulkArchiving] = useMutationWithToasts( bulkArchiveMutation, @@ -163,7 +164,6 @@ export function DocumentList(props: { { successMessage: __("Documents unarchived successfully."), errorMessage: __("Failed to unarchive documents") }, ); const { list: selection, toggle, clear, reset } = useList([]); - const confirm = useConfirm(); const canDeleteAny = documents.some(({ canDelete }) => canDelete); const canUpdateAny = documents.some(({ canUpdate }) => canUpdate); @@ -247,28 +247,7 @@ export function DocumentList(props: { }; const handleBulkDelete = () => { - const documentCount = selection.length; - confirm( - () => - bulkDeleteDocuments({ - variables: { input: { documentIds: selection } }, - updater: (store) => { - const conn = store.get(connectionId); - if (conn) { - selection.forEach(id => ConnectionHandler.deleteNode(conn, id)); - } - }, - }).then(() => { - clear(); - }), - { - message: sprintf( - __("This will permanently delete %s document%s. This action cannot be undone."), - documentCount, - documentCount > 1 ? "s" : "", - ), - }, - ); + deleteDocumentsDialogRef.current?.open(); }; const handleBulkArchive = () => { @@ -342,6 +321,12 @@ export function DocumentList(props: { return (
+