mirror of
https://github.com/documenso/documenso.git
synced 2026-07-15 07:17:09 +10:00
4f346d3c2d
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.
255 lines
9.3 KiB
TypeScript
255 lines
9.3 KiB
TypeScript
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
|
import { getEnvelopeItemPermissions, mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
|
import { Badge } from '@documenso/ui/primitives/badge';
|
|
import { Button } from '@documenso/ui/primitives/button';
|
|
import { Separator } from '@documenso/ui/primitives/separator';
|
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
import { DocumentStatus, EnvelopeType, TemplateType } from '@prisma/client';
|
|
import {
|
|
AlertTriangleIcon,
|
|
Building2Icon,
|
|
Globe2Icon,
|
|
LockIcon,
|
|
RefreshCwIcon,
|
|
SendIcon,
|
|
SettingsIcon,
|
|
} from 'lucide-react';
|
|
import { useMemo } from 'react';
|
|
import { Link } from 'react-router';
|
|
import { match } from 'ts-pattern';
|
|
|
|
import { EnvelopeDistributeDialog } from '~/components/dialogs/envelope-distribute-dialog';
|
|
import { EnvelopeRedistributeDialog } from '~/components/dialogs/envelope-redistribute-dialog';
|
|
import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog';
|
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
|
import { DocumentAttachmentsPopover } from '~/components/general/document/document-attachments-popover';
|
|
import { EmbeddedEditorAttachmentPopover } from '~/components/general/document/embedded-editor-attachment-popover';
|
|
import { EnvelopeEditorSettingsDialog } from '~/components/general/envelope-editor/envelope-editor-settings-dialog';
|
|
|
|
import { TemplateDirectLinkBadge } from '../template/template-direct-link-badge';
|
|
import { EnvelopeItemTitleInput } from './envelope-editor-title-input';
|
|
|
|
export default function EnvelopeEditorHeader() {
|
|
const { t } = useLingui();
|
|
|
|
const {
|
|
envelope,
|
|
isDocument,
|
|
isTemplate,
|
|
isEmbedded,
|
|
updateEnvelope,
|
|
autosaveError,
|
|
relativePath,
|
|
editorConfig,
|
|
flushAutosave,
|
|
} = useCurrentEnvelopeEditor();
|
|
|
|
const {
|
|
embedded,
|
|
general: { allowConfigureEnvelopeTitle },
|
|
actions: { allowAttachments, allowDistributing },
|
|
} = editorConfig;
|
|
|
|
const envelopeItemPermissions = useMemo(
|
|
() => getEnvelopeItemPermissions(envelope, envelope.recipients),
|
|
[envelope, envelope.recipients],
|
|
);
|
|
|
|
const handleCreateEmbeddedEnvelope = async () => {
|
|
const latestEnvelope = await flushAutosave();
|
|
|
|
embedded?.onCreate?.(latestEnvelope);
|
|
};
|
|
|
|
const handleUpdateEmbeddedEnvelope = async () => {
|
|
const latestEnvelope = await flushAutosave();
|
|
|
|
embedded?.onUpdate?.(latestEnvelope);
|
|
};
|
|
|
|
return (
|
|
<nav className="w-full border-border border-b bg-background px-4 py-3 md:px-6">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex min-w-0 flex-1 items-center space-x-4">
|
|
{editorConfig.embedded?.customBrandingLogo ? (
|
|
<img src={`/api/branding/logo/team/${envelope.teamId}`} alt="Logo" className="h-6 w-auto" />
|
|
) : (
|
|
<Link to="/">
|
|
<BrandingLogo className="h-6 w-auto" />
|
|
</Link>
|
|
)}
|
|
<Separator orientation="vertical" className="h-6 shrink-0" />
|
|
|
|
<div className="flex min-w-0 items-center space-x-2">
|
|
<EnvelopeItemTitleInput
|
|
dataTestId="envelope-title-input"
|
|
disabled={!envelopeItemPermissions.canTitleBeChanged || !allowConfigureEnvelopeTitle}
|
|
value={envelope.title}
|
|
onChange={(title) => {
|
|
updateEnvelope({
|
|
data: {
|
|
title,
|
|
},
|
|
});
|
|
}}
|
|
placeholder={t`Envelope Title`}
|
|
/>
|
|
|
|
{envelope.type === EnvelopeType.TEMPLATE && (
|
|
<>
|
|
{envelope.templateType === TemplateType.PRIVATE && (
|
|
<Badge variant="secondary" className="shrink-0">
|
|
<LockIcon className="mr-2 h-4 w-4 text-blue-600 dark:text-blue-300" />
|
|
<Trans>Private Template</Trans>
|
|
</Badge>
|
|
)}
|
|
{envelope.templateType === TemplateType.ORGANISATION && (
|
|
<Badge variant="orange" className="shrink-0">
|
|
<Building2Icon className="mr-2 size-4" />
|
|
<Trans>Organisation Template</Trans>
|
|
</Badge>
|
|
)}
|
|
{envelope.templateType === TemplateType.PUBLIC && (
|
|
<Badge variant="default" className="shrink-0">
|
|
<Globe2Icon className="mr-2 h-4 w-4 text-green-500 dark:text-green-300" />
|
|
<Trans>Public Template</Trans>
|
|
</Badge>
|
|
)}
|
|
|
|
{envelope.directLink?.token && (
|
|
<TemplateDirectLinkBadge
|
|
className="shrink-0 py-1"
|
|
token={envelope.directLink.token}
|
|
enabled={envelope.directLink.enabled}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{envelope.type === EnvelopeType.DOCUMENT &&
|
|
match(envelope.status)
|
|
.with(DocumentStatus.DRAFT, () => (
|
|
<Badge variant="warning" className="shrink-0">
|
|
<Trans>Draft</Trans>
|
|
</Badge>
|
|
))
|
|
.with(DocumentStatus.PENDING, () => (
|
|
<Badge variant="secondary" className="shrink-0">
|
|
<Trans>Pending</Trans>
|
|
</Badge>
|
|
))
|
|
.with(DocumentStatus.COMPLETED, () => (
|
|
<Badge variant="default" className="shrink-0">
|
|
<Trans>Completed</Trans>
|
|
</Badge>
|
|
))
|
|
.with(DocumentStatus.REJECTED, () => (
|
|
<Badge variant="destructive" className="shrink-0">
|
|
<Trans>Rejected</Trans>
|
|
</Badge>
|
|
))
|
|
.with(DocumentStatus.CANCELLED, () => (
|
|
<Badge variant="destructive" className="shrink-0">
|
|
<Trans>Cancelled</Trans>
|
|
</Badge>
|
|
))
|
|
.exhaustive()}
|
|
|
|
{autosaveError && (
|
|
<>
|
|
<Badge variant="destructive" className="shrink-0">
|
|
<AlertTriangleIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Sync failed, changes not saved</Trans>
|
|
</Badge>
|
|
|
|
<button
|
|
onClick={() => {
|
|
window.location.reload();
|
|
}}
|
|
>
|
|
<Badge variant="destructive" className="shrink-0">
|
|
<RefreshCwIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Reload</Trans>
|
|
</Badge>
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 items-center space-x-2">
|
|
{allowAttachments &&
|
|
(isEmbedded ? (
|
|
<EmbeddedEditorAttachmentPopover buttonSize="sm" />
|
|
) : (
|
|
<DocumentAttachmentsPopover envelopeId={envelope.id} buttonSize="sm" />
|
|
))}
|
|
|
|
{editorConfig.settings && (
|
|
<EnvelopeEditorSettingsDialog
|
|
trigger={
|
|
<Button variant="outline" size="sm">
|
|
<SettingsIcon className="h-4 w-4" />
|
|
</Button>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{match({ isEmbedded, isDocument, isTemplate, allowDistributing })
|
|
.with({ isEmbedded: false, isDocument: true, allowDistributing: true }, () => (
|
|
<>
|
|
<EnvelopeDistributeDialog
|
|
documentRootPath={relativePath.documentRootPath}
|
|
trigger={
|
|
<Button size="sm">
|
|
<SendIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Send Document</Trans>
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<EnvelopeRedistributeDialog
|
|
envelope={envelope}
|
|
trigger={
|
|
<Button size="sm">
|
|
<SendIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Resend Document</Trans>
|
|
</Button>
|
|
}
|
|
/>
|
|
</>
|
|
))
|
|
.with({ isEmbedded: false, isTemplate: true, allowDistributing: true }, () => (
|
|
<TemplateUseDialog
|
|
envelopeId={envelope.id}
|
|
templateId={mapSecondaryIdToTemplateId(envelope.secondaryId)}
|
|
templateSigningOrder={envelope.documentMeta?.signingOrder}
|
|
recipients={envelope.recipients}
|
|
documentRootPath={relativePath.documentRootPath}
|
|
trigger={
|
|
<Button size="sm">
|
|
<Trans>Use Template</Trans>
|
|
</Button>
|
|
}
|
|
/>
|
|
))
|
|
|
|
.otherwise(() => null)}
|
|
|
|
{embedded?.mode === 'create' && (
|
|
<Button size="sm" onClick={handleCreateEmbeddedEnvelope}>
|
|
{isDocument ? <Trans>Create Document</Trans> : <Trans>Create Template</Trans>}
|
|
</Button>
|
|
)}
|
|
|
|
{embedded?.mode === 'edit' && (
|
|
<Button size="sm" onClick={handleUpdateEmbeddedEnvelope}>
|
|
{isDocument ? <Trans>Update Document</Trans> : <Trans>Update Template</Trans>}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|