From 4f346d3c2d5264f221e4d787e162f16051e44114 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 18 Jun 2026 13:52:35 +1000 Subject: [PATCH 1/3] 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. --- .../dialogs/envelope-cancel-dialog.tsx | 134 +++++++ .../dialogs/envelope-delete-dialog.tsx | 2 +- .../dialogs/envelopes-bulk-cancel-dialog.tsx | 159 ++++++++ .../general/document/document-status.tsx | 6 + .../envelope-editor-header.tsx | 5 + .../documents-table-action-dropdown.tsx | 32 +- .../tables/documents-table-empty-state.tsx | 7 +- .../envelopes-table-bulk-action-bar.tsx | 11 +- .../t.$teamUrl+/documents.$id._index.tsx | 1 + .../t.$teamUrl+/documents._index.tsx | 12 + .../embed+/v1+/authoring+/document.create.tsx | 18 +- .../embed+/v1+/authoring+/template.create.tsx | 18 +- apps/remix/server/api/files/files.helpers.ts | 25 ++ apps/remix/server/api/files/files.ts | 21 +- apps/remix/server/api/files/files.types.ts | 8 + packages/api/v1/implementation.ts | 9 +- .../api-access-envelope-cancel.spec.ts | 242 +++++++++++++ .../api-access-file-upload.spec.ts | 102 ++++++ .../documents/bulk-document-actions.spec.ts | 151 +++++++- .../e2e/documents/cancel-documents.spec.ts | 342 ++++++++++++++++++ packages/lib/constants/document.ts | 3 + packages/lib/errors/app-error.ts | 3 + .../server-only/admin/get-documents-stats.ts | 1 + .../server-only/document/cancel-document.ts | 129 +++++++ .../server-only/document/find-documents.ts | 28 +- .../lib/server-only/document/get-stats.ts | 20 +- .../envelope/assert-envelope-mutable.ts | 6 +- packages/lib/types/document-audit-logs.ts | 12 + packages/lib/universal/upload/put-file.ts | 32 +- packages/lib/utils/document-audit-logs.ts | 8 + packages/lib/utils/document.ts | 4 +- packages/lib/utils/envelope.ts | 5 +- packages/lib/utils/teams.ts | 10 +- .../migration.sql | 1 + packages/prisma/schema.prisma | 1 + packages/prisma/seed/documents.ts | 85 +++++ .../document-router/download-document-beta.ts | 9 +- .../download-document-certificate.ts | 6 +- .../find-documents-internal.types.ts | 1 + .../envelope-router/bulk-cancel-envelopes.ts | 95 +++++ .../bulk-cancel-envelopes.types.ts | 21 ++ .../server/envelope-router/cancel-envelope.ts | 37 ++ .../envelope-router/cancel-envelope.types.ts | 23 ++ .../trpc/server/envelope-router/router.ts | 4 + .../primitives/document-flow/add-subject.tsx | 2 + 45 files changed, 1806 insertions(+), 45 deletions(-) create mode 100644 apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx create mode 100644 packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-envelope-cancel.spec.ts create mode 100644 packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-file-upload.spec.ts create mode 100644 packages/app-tests/e2e/documents/cancel-documents.spec.ts create mode 100644 packages/lib/server-only/document/cancel-document.ts create mode 100644 packages/prisma/migrations/20260616120000_add_cancelled_document_status/migration.sql create mode 100644 packages/trpc/server/envelope-router/bulk-cancel-envelopes.ts create mode 100644 packages/trpc/server/envelope-router/bulk-cancel-envelopes.types.ts create mode 100644 packages/trpc/server/envelope-router/cancel-envelope.ts create mode 100644 packages/trpc/server/envelope-router/cancel-envelope.types.ts diff --git a/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx b/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx new file mode 100644 index 000000000..8cb90cfa0 --- /dev/null +++ b/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx @@ -0,0 +1,134 @@ +import { trpc as trpcReact } from '@documenso/trpc/react'; +import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { Label } from '@documenso/ui/primitives/label'; +import { Textarea } from '@documenso/ui/primitives/textarea'; +import { useToast } from '@documenso/ui/primitives/use-toast'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useEffect, useState } from 'react'; + +export type EnvelopeCancelDialogProps = { + id: string; + title: string; + trigger?: React.ReactNode; + onCancel?: () => Promise | void; +}; + +export const EnvelopeCancelDialog = ({ id, title, trigger, onCancel }: EnvelopeCancelDialogProps) => { + const { toast } = useToast(); + const { t } = useLingui(); + const trpcUtils = trpcReact.useUtils(); + + const [open, setOpen] = useState(false); + const [reason, setReason] = useState(''); + + const { mutateAsync: cancelEnvelope, isPending } = trpcReact.envelope.cancel.useMutation({ + onSuccess: async () => { + toast({ + title: t`Document cancelled`, + description: t`"${title}" has been successfully cancelled`, + duration: 5000, + }); + + await trpcUtils.document.findDocumentsInternal.invalidate(); + + await onCancel?.(); + + setOpen(false); + }, + onError: () => { + toast({ + title: t`Something went wrong`, + description: t`This document could not be cancelled at this time. Please try again.`, + variant: 'destructive', + duration: 7500, + }); + }, + }); + + useEffect(() => { + if (open) { + setReason(''); + } + }, [open]); + + return ( + !isPending && setOpen(value)}> + {trigger} + + + + + Are you sure? + + + + + You are about to cancel "{title}" + + + + + + +

+ Once confirmed, the following will occur: +

+ +
    +
  • + The document signing process will be stopped +
  • +
  • + Recipients will be notified that the document was cancelled +
  • +
  • + The document will remain in your dashboard marked as Cancelled +
  • +
+
+
+ +
+ + +