mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
feat: per-recipient envelope expiration (#2519)
This commit is contained in:
@@ -34,7 +34,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.RecipientSigned}
|
||||
>
|
||||
<Trans>Email the owner when a recipient signs</Trans>
|
||||
@@ -44,7 +44,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient signed email</Trans>
|
||||
@@ -72,7 +72,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.RecipientSigningRequest}
|
||||
>
|
||||
<Trans>Email recipients with a signing request</Trans>
|
||||
@@ -82,7 +82,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient signing request email</Trans>
|
||||
@@ -110,7 +110,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.RecipientRemoved}
|
||||
>
|
||||
<Trans>Email recipients when they're removed from a pending document</Trans>
|
||||
@@ -120,7 +120,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient removed email</Trans>
|
||||
@@ -148,7 +148,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.DocumentPending}
|
||||
>
|
||||
<Trans>Email the signer if the document is still pending</Trans>
|
||||
@@ -158,7 +158,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document pending email</Trans>
|
||||
@@ -187,7 +187,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.DocumentCompleted}
|
||||
>
|
||||
<Trans>Email recipients when the document is completed</Trans>
|
||||
@@ -197,7 +197,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document completed email</Trans>
|
||||
@@ -225,7 +225,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.DocumentDeleted}
|
||||
>
|
||||
<Trans>Email recipients when a pending document is deleted</Trans>
|
||||
@@ -235,7 +235,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document deleted email</Trans>
|
||||
@@ -263,7 +263,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.OwnerDocumentCompleted}
|
||||
>
|
||||
<Trans>Email the owner when the document is completed</Trans>
|
||||
@@ -273,7 +273,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document completed email</Trans>
|
||||
@@ -290,6 +290,45 @@ export const DocumentEmailCheckboxes = ({
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
id={DocumentEmailEvents.OwnerRecipientExpired}
|
||||
className="h-5 w-5"
|
||||
checked={value.ownerRecipientExpired}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.OwnerRecipientExpired]: Boolean(checked) })
|
||||
}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
|
||||
htmlFor={DocumentEmailEvents.OwnerRecipientExpired}
|
||||
>
|
||||
<Trans>Send recipient expired email to the owner</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient expired email</Trans>
|
||||
</strong>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This will be sent to the document owner when a recipient's signing window has
|
||||
expired.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import type {
|
||||
TEnvelopeExpirationDurationPeriod,
|
||||
TEnvelopeExpirationPeriod,
|
||||
} from '@documenso/lib/constants/envelope-expiration';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
const EXPIRATION_UNITS: Array<{
|
||||
value: TEnvelopeExpirationDurationPeriod['unit'];
|
||||
label: MessageDescriptor;
|
||||
}> = [
|
||||
{ value: 'day', label: msg`Days` },
|
||||
{ value: 'week', label: msg`Weeks` },
|
||||
{ value: 'month', label: msg`Months` },
|
||||
{ value: 'year', label: msg`Years` },
|
||||
];
|
||||
|
||||
type ExpirationMode = 'duration' | 'disabled' | 'inherit';
|
||||
|
||||
const getMode = (value: TEnvelopeExpirationPeriod | null | undefined): ExpirationMode => {
|
||||
if (!value) {
|
||||
return 'inherit';
|
||||
}
|
||||
|
||||
if ('disabled' in value) {
|
||||
return 'disabled';
|
||||
}
|
||||
|
||||
return 'duration';
|
||||
};
|
||||
|
||||
const getAmount = (value: TEnvelopeExpirationPeriod | null | undefined): number => {
|
||||
if (value && 'amount' in value) {
|
||||
return value.amount;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getUnit = (
|
||||
value: TEnvelopeExpirationPeriod | null | undefined,
|
||||
): TEnvelopeExpirationDurationPeriod['unit'] => {
|
||||
if (value && 'unit' in value) {
|
||||
return value.unit;
|
||||
}
|
||||
|
||||
return 'month';
|
||||
};
|
||||
|
||||
export type ExpirationPeriodPickerProps = {
|
||||
value: TEnvelopeExpirationPeriod | null | undefined;
|
||||
onChange: (value: TEnvelopeExpirationPeriod | null) => void;
|
||||
disabled?: boolean;
|
||||
inheritLabel?: string;
|
||||
};
|
||||
|
||||
export const ExpirationPeriodPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
inheritLabel,
|
||||
}: ExpirationPeriodPickerProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const mode = getMode(value);
|
||||
const amount = getAmount(value);
|
||||
const unit = getUnit(value);
|
||||
|
||||
const onModeChange = (newMode: string) => {
|
||||
if (newMode === 'inherit') {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMode === 'disabled') {
|
||||
onChange({ disabled: true });
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({ unit, amount });
|
||||
};
|
||||
|
||||
const onAmountChange = (newAmount: number) => {
|
||||
const clamped = Math.max(1, Math.floor(newAmount));
|
||||
|
||||
onChange({ unit, amount: clamped });
|
||||
};
|
||||
|
||||
const onUnitChange = (newUnit: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
onChange({ unit: newUnit as TEnvelopeExpirationDurationPeriod['unit'], amount });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select value={mode} onValueChange={onModeChange} disabled={disabled}>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="duration">
|
||||
<Trans>Custom duration</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="disabled">
|
||||
<Trans>Never expires</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{inheritLabel !== undefined && <SelectItem value="inherit">{inheritLabel}</SelectItem>}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{mode === 'duration' && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-20 bg-background"
|
||||
value={amount}
|
||||
onChange={(e) => onAmountChange(Number(e.target.value))}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Select value={unit} onValueChange={onUnitChange} disabled={disabled}>
|
||||
<SelectTrigger className="flex-1 bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{EXPIRATION_UNITS.map((u) => (
|
||||
<SelectItem key={u.value} value={u.value}>
|
||||
{_(u.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user