feat: add cancellable document status (#2992)

Adds a CANCELLED envelope status that privileged members (owner or team
admin/manager) can move a pending document into. Sending recipient
notifications via a background job while retaining the document in the
dashboard as proof of distribution.

Includes a dedicated Cancelled tab, single and bulk cancel actions,
the ENVELOPE_CANCELLED mutability guard, and e2e coverage for
permissions
and visibility.
This commit is contained in:
Lucas Smith
2026-06-18 13:52:35 +10:00
committed by GitHub
parent d5ce222482
commit 4f346d3c2d
45 changed files with 1806 additions and 45 deletions
@@ -224,6 +224,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
{match(envelope.status)
.with(DocumentStatus.COMPLETED, () => <Trans>This document has been signed by all recipients</Trans>)
.with(DocumentStatus.REJECTED, () => <Trans>This document has been rejected by a recipient</Trans>)
.with(DocumentStatus.CANCELLED, () => <Trans>This document has been cancelled</Trans>)
.with(DocumentStatus.DRAFT, () => (
<Trans>This document is currently a draft and has not been sent</Trans>
))
@@ -20,6 +20,7 @@ import { Link, useParams, useSearchParams } from 'react-router';
import { z } from 'zod';
import { DocumentMoveToFolderDialog } from '~/components/dialogs/document-move-to-folder-dialog';
import { EnvelopesBulkCancelDialog } from '~/components/dialogs/envelopes-bulk-cancel-dialog';
import { EnvelopesBulkDeleteDialog } from '~/components/dialogs/envelopes-bulk-delete-dialog';
import { EnvelopesBulkMoveDialog } from '~/components/dialogs/envelopes-bulk-move-dialog';
import { DocumentSearch } from '~/components/general/document/document-search';
@@ -61,6 +62,7 @@ export default function DocumentsPage() {
const [rowSelection, setRowSelection] = useSessionStorage<RowSelectionState>('documents-bulk-selection', {});
const [isBulkMoveDialogOpen, setIsBulkMoveDialogOpen] = useState(false);
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
const [isBulkCancelDialogOpen, setIsBulkCancelDialogOpen] = useState(false);
const selectedEnvelopeIds = useMemo(() => {
return Object.keys(rowSelection).filter((id) => rowSelection[id]);
@@ -71,6 +73,7 @@ export default function DocumentsPage() {
[ExtendedDocumentStatus.PENDING]: 0,
[ExtendedDocumentStatus.COMPLETED]: 0,
[ExtendedDocumentStatus.REJECTED]: 0,
[ExtendedDocumentStatus.CANCELLED]: 0,
[ExtendedDocumentStatus.INBOX]: 0,
[ExtendedDocumentStatus.ALL]: 0,
});
@@ -150,6 +153,7 @@ export default function DocumentsPage() {
ExtendedDocumentStatus.INBOX,
ExtendedDocumentStatus.PENDING,
ExtendedDocumentStatus.COMPLETED,
ExtendedDocumentStatus.CANCELLED,
ExtendedDocumentStatus.DRAFT,
ExtendedDocumentStatus.ALL,
]
@@ -227,6 +231,7 @@ export default function DocumentsPage() {
selectedCount={selectedEnvelopeIds.length}
onMoveClick={() => setIsBulkMoveDialogOpen(true)}
onDeleteClick={() => setIsBulkDeleteDialogOpen(true)}
onCancelClick={() => setIsBulkCancelDialogOpen(true)}
onClearSelection={() => setRowSelection({})}
/>
@@ -246,6 +251,13 @@ export default function DocumentsPage() {
onOpenChange={setIsBulkDeleteDialogOpen}
onSuccess={() => setRowSelection({})}
/>
<EnvelopesBulkCancelDialog
envelopeIds={selectedEnvelopeIds}
open={isBulkCancelDialogOpen}
onOpenChange={setIsBulkCancelDialogOpen}
onSuccess={() => setRowSelection({})}
/>
</div>
</EnvelopeDropZoneWrapper>
);