mirror of
https://github.com/documenso/documenso.git
synced 2026-07-14 23:07:13 +10:00
chore: merge main, resolve biome formatting conflicts
Merge origin/main into feat/external-2fa-codes. Resolve formatting conflicts caused by biome rollout; preserve both feature streams: PR's external 2FA token + signing-session 2FA proof additions plus main's RateLimit/RecipientExpired/signingReminders/date-auto-insert. In complete-document-with-token.ts, drop the duplicate early field-fetching block introduced when main moved that logic later with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH check using derivedRecipientActionAuth.
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import type { TDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||
import { DocumentEmailEvents } from '@documenso/lib/types/document-email';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Checkbox } from '../../primitives/checkbox';
|
||||
@@ -16,11 +15,7 @@ type DocumentEmailCheckboxesProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const DocumentEmailCheckboxes = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}: DocumentEmailCheckboxesProps) => {
|
||||
export const DocumentEmailCheckboxes = ({ value, onChange, className }: DocumentEmailCheckboxesProps) => {
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
<div className="flex flex-row items-center">
|
||||
@@ -28,23 +23,21 @@ export const DocumentEmailCheckboxes = ({
|
||||
id={DocumentEmailEvents.RecipientSigned}
|
||||
className="h-5 w-5"
|
||||
checked={value.recipientSigned}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.RecipientSigned]: Boolean(checked) })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...value, [DocumentEmailEvents.RecipientSigned]: Boolean(checked) })}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-muted-foreground text-sm"
|
||||
htmlFor={DocumentEmailEvents.RecipientSigned}
|
||||
>
|
||||
<Trans>Send recipient signed email</Trans>
|
||||
<Trans>Email the owner when a recipient signs</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<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>
|
||||
@@ -52,9 +45,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This email is sent to the document owner when a recipient has signed the document.
|
||||
</Trans>
|
||||
<Trans>This email is sent to the document owner when a recipient has signed the document.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -72,17 +63,17 @@ 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-muted-foreground text-sm"
|
||||
htmlFor={DocumentEmailEvents.RecipientSigningRequest}
|
||||
>
|
||||
<Trans>Send recipient signing request email</Trans>
|
||||
<Trans>Email recipients with a signing request</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<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>
|
||||
@@ -90,9 +81,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This email is sent to the recipient requesting them to sign the document.
|
||||
</Trans>
|
||||
<Trans>This email is sent to the recipient requesting them to sign the document.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -110,17 +99,17 @@ 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-muted-foreground text-sm"
|
||||
htmlFor={DocumentEmailEvents.RecipientRemoved}
|
||||
>
|
||||
<Trans>Send recipient removed email</Trans>
|
||||
<Trans>Email recipients when they're removed from a pending document</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<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>
|
||||
@@ -128,9 +117,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This email is sent to the recipient if they are removed from a pending document.
|
||||
</Trans>
|
||||
<Trans>This email is sent to the recipient if they are removed from a pending document.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -142,23 +129,21 @@ export const DocumentEmailCheckboxes = ({
|
||||
id={DocumentEmailEvents.DocumentPending}
|
||||
className="h-5 w-5"
|
||||
checked={value.documentPending}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.DocumentPending]: Boolean(checked) })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...value, [DocumentEmailEvents.DocumentPending]: Boolean(checked) })}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-muted-foreground text-sm"
|
||||
htmlFor={DocumentEmailEvents.DocumentPending}
|
||||
>
|
||||
<Trans>Send document pending email</Trans>
|
||||
<Trans>Email the signer if the document is still pending</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<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>
|
||||
@@ -167,8 +152,8 @@ export const DocumentEmailCheckboxes = ({
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This email will be sent to the recipient who has just signed the document, if
|
||||
there are still other recipients who have not signed yet.
|
||||
This email will be sent to the recipient who has just signed the document, if there are still other
|
||||
recipients who have not signed yet.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
@@ -187,17 +172,17 @@ 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-muted-foreground text-sm"
|
||||
htmlFor={DocumentEmailEvents.DocumentCompleted}
|
||||
>
|
||||
<Trans>Send document completed email</Trans>
|
||||
<Trans>Email recipients when the document is completed</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<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>
|
||||
@@ -205,9 +190,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This will be sent to all recipients once the document has been fully completed.
|
||||
</Trans>
|
||||
<Trans>This will be sent to all recipients once the document has been fully completed.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -219,23 +202,21 @@ export const DocumentEmailCheckboxes = ({
|
||||
id={DocumentEmailEvents.DocumentDeleted}
|
||||
className="h-5 w-5"
|
||||
checked={value.documentDeleted}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.DocumentDeleted]: Boolean(checked) })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...value, [DocumentEmailEvents.DocumentDeleted]: Boolean(checked) })}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
className="ml-2 flex flex-row items-center text-muted-foreground text-sm"
|
||||
htmlFor={DocumentEmailEvents.DocumentDeleted}
|
||||
>
|
||||
<Trans>Send document deleted email</Trans>
|
||||
<Trans>Email recipients when a pending document is deleted</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<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>
|
||||
@@ -243,9 +224,7 @@ export const DocumentEmailCheckboxes = ({
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This will be sent to all recipients if a pending document has been deleted.
|
||||
</Trans>
|
||||
<Trans>This will be sent to all recipients if a pending document has been deleted.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -263,33 +242,105 @@ 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-muted-foreground text-sm"
|
||||
htmlFor={DocumentEmailEvents.OwnerDocumentCompleted}
|
||||
>
|
||||
<Trans>Send document completed email to the owner</Trans>
|
||||
<Trans>Email the owner when the document is completed</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<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>
|
||||
</strong>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>This will be sent to the document owner once the document has been fully completed.</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
id={DocumentEmailEvents.OwnerDocumentCreated}
|
||||
className="h-5 w-5"
|
||||
checked={value.ownerDocumentCreated}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.OwnerDocumentCreated]: Boolean(checked) })
|
||||
}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="ml-2 flex flex-row items-center text-muted-foreground text-sm"
|
||||
htmlFor={DocumentEmailEvents.OwnerDocumentCreated}
|
||||
>
|
||||
<Trans>Email the owner when a document is created from a direct template</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>Document created from direct template email</Trans>
|
||||
</strong>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This will be sent to the document owner once the document has been fully
|
||||
completed.
|
||||
This email is sent to the document owner when a recipient creates a document via a direct template
|
||||
link.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</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-muted-foreground text-sm"
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
export interface DocumentGlobalAuthAccessSelectProps {
|
||||
value?: string[];
|
||||
@@ -41,15 +38,11 @@ export const DocumentGlobalAuthAccessSelect = ({
|
||||
|
||||
// Convert string array to Option array for MultiSelect
|
||||
const selectedOptions =
|
||||
(value
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
(value?.map((val) => authOptions.find((option) => option.value === val)).filter(Boolean) as Option[]) || [];
|
||||
|
||||
// Convert default value to Option array
|
||||
const defaultOptions =
|
||||
(defaultValue
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
(defaultValue?.map((val) => authOptions.find((option) => option.value === val)).filter(Boolean) as Option[]) || [];
|
||||
|
||||
const handleChange = (options: Option[]) => {
|
||||
const values = options.map((option) => option.value);
|
||||
@@ -79,7 +72,7 @@ export const DocumentGlobalAuthAccessTooltip = () => (
|
||||
<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 access</Trans>
|
||||
@@ -102,8 +95,7 @@ export const DocumentGlobalAuthAccessTooltip = () => (
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>No restrictions</strong> - The document can be accessed directly by the URL sent
|
||||
to the recipient
|
||||
<strong>No restrictions</strong> - The document can be accessed directly by the URL sent to the recipient
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { DocumentActionAuth, DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
export interface DocumentGlobalAuthActionSelectProps {
|
||||
value?: string[];
|
||||
@@ -41,15 +40,11 @@ export const DocumentGlobalAuthActionSelect = ({
|
||||
|
||||
// Convert string array to Option array for MultiSelect
|
||||
const selectedOptions =
|
||||
(value
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
(value?.map((val) => authOptions.find((option) => option.value === val)).filter(Boolean) as Option[]) || [];
|
||||
|
||||
// Convert default value to Option array
|
||||
const defaultOptions =
|
||||
(defaultValue
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
(defaultValue?.map((val) => authOptions.find((option) => option.value === val)).filter(Boolean) as Option[]) || [];
|
||||
|
||||
const handleChange = (options: Option[]) => {
|
||||
const values = options.map((option) => option.value);
|
||||
@@ -79,42 +74,39 @@ export const DocumentGlobalAuthActionTooltip = () => (
|
||||
<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>
|
||||
<Trans>Global recipient action authentication</Trans>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
The authentication methods required for recipients to sign the signature field.
|
||||
</Trans>
|
||||
<Trans>The authentication methods required for recipients to sign the signature field.</Trans>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
These can be overriden by setting the authentication requirements directly on each
|
||||
recipient in the next step. Multiple methods can be selected.
|
||||
These can be overriden by setting the authentication requirements directly on each recipient in the next step.
|
||||
Multiple methods can be selected.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
||||
configured via their settings
|
||||
<strong>Require passkey</strong> - The recipient must have an account and passkey configured via their
|
||||
settings
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require 2FA</strong> - The recipient must have an account and 2FA enabled via
|
||||
their settings
|
||||
<strong>Require 2FA</strong> - The recipient must have an account and 2FA enabled via their settings
|
||||
</Trans>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require password</strong> - The recipient must have an account and password
|
||||
configured via their settings, the password will be verified during signing
|
||||
<strong>Require password</strong> - The recipient must have an account and password configured via their
|
||||
settings, the password will be verified during signing
|
||||
</Trans>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentMeta, Field, Recipient } from '@prisma/client';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { Clock, EyeOffIcon } from 'lucide-react';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
|
||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||
@@ -17,6 +10,12 @@ import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentMeta, Field } from '@prisma/client';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { Clock, EyeOffIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { getRecipientColorStyles } from '../../lib/recipient-colors';
|
||||
import { FieldContent } from '../../primitives/document-flow/field-content';
|
||||
@@ -34,7 +33,7 @@ const getRecipientDisplayText = (recipient: { name: string; email: string }) =>
|
||||
};
|
||||
|
||||
export type DocumentField = Field & {
|
||||
recipient: Pick<Recipient, 'name' | 'email' | 'signingStatus'>;
|
||||
recipient: Pick<TRecipientLite, 'name' | 'email' | 'signingStatus'>;
|
||||
};
|
||||
|
||||
export type DocumentReadOnlyFieldsProps = {
|
||||
@@ -66,10 +65,7 @@ export type DocumentReadOnlyFieldsProps = {
|
||||
showRecipientColors?: boolean;
|
||||
};
|
||||
|
||||
export const mapFieldsWithRecipients = (
|
||||
fields: Field[],
|
||||
recipients: Recipient[],
|
||||
): DocumentField[] => {
|
||||
export const mapFieldsWithRecipients = (fields: Field[], recipients: TRecipientLite[]): DocumentField[] => {
|
||||
return fields.map((field) => {
|
||||
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
|
||||
id: field.recipientId,
|
||||
@@ -98,10 +94,8 @@ export const DocumentReadOnlyFields = ({
|
||||
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
|
||||
};
|
||||
|
||||
const highestPageNumber = Math.max(...fields.map((field) => field.page));
|
||||
|
||||
return (
|
||||
<ElementVisible target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}>
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map(
|
||||
(field) =>
|
||||
!hiddenFieldIds[field.secondaryId] && (
|
||||
@@ -111,21 +105,16 @@ export const DocumentReadOnlyFields = ({
|
||||
readonly={true}
|
||||
color={
|
||||
showRecipientColors
|
||||
? getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipientIds.findIndex((id) => id === field.recipientId),
|
||||
0,
|
||||
),
|
||||
)
|
||||
? getRecipientColorStyles(recipientIds.findIndex((id) => id === field.recipientId))
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{showRecipientTooltip && (
|
||||
<div className="absolute -right-3 -top-3">
|
||||
<div className="absolute -top-3 -right-3">
|
||||
<PopoverHover
|
||||
trigger={
|
||||
<Avatar className="h-6 w-6 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
|
||||
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
|
||||
<Avatar className="h-6 w-6 border-2 border-gray-200/50 border-solid transition-colors hover:border-gray-200">
|
||||
<AvatarFallback className="bg-neutral-50 text-gray-400 text-xs">
|
||||
{extractInitials(field.recipient.name || field.recipient.email)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -137,11 +126,7 @@ export const DocumentReadOnlyFields = ({
|
||||
{showFieldStatus && (
|
||||
<Badge
|
||||
className="mx-auto mb-1 py-0.5"
|
||||
variant={
|
||||
field.recipient.signingStatus === SigningStatus.SIGNED
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
}
|
||||
variant={field.recipient.signingStatus === SigningStatus.SIGNED ? 'default' : 'secondary'}
|
||||
>
|
||||
{field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
||||
<>
|
||||
@@ -158,17 +143,15 @@ export const DocumentReadOnlyFields = ({
|
||||
)}
|
||||
|
||||
<p className="text-center font-semibold">
|
||||
<span>
|
||||
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field
|
||||
</span>
|
||||
<span>{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
<p className="mt-1 text-center text-muted-foreground text-xs">
|
||||
{getRecipientDisplayText(field.recipient)}
|
||||
</p>
|
||||
|
||||
<button
|
||||
className="absolute right-0 top-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
|
||||
className="absolute top-0 right-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
|
||||
onClick={() => handleHideField(field.secondaryId)}
|
||||
title="Hide field"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
export const DocumentSendEmailMessageHelper = () => {
|
||||
@@ -11,21 +9,15 @@ export const DocumentSendEmailMessageHelper = () => {
|
||||
|
||||
<ul className="mt-2 flex list-inside list-disc flex-col gap-y-2 text-sm">
|
||||
<li className="text-muted-foreground">
|
||||
<code className="text-muted-foreground bg-muted-foreground/20 rounded p-1 text-sm">
|
||||
{'{signer.name}'}
|
||||
</code>{' '}
|
||||
- <Trans>The signer's name</Trans>
|
||||
<code className="rounded bg-muted-foreground/20 p-1 text-muted-foreground text-sm">{'{signer.name}'}</code> -{' '}
|
||||
<Trans>The signer's name</Trans>
|
||||
</li>
|
||||
<li className="text-muted-foreground">
|
||||
<code className="text-muted-foreground bg-muted-foreground/20 rounded p-1 text-sm">
|
||||
{'{signer.email}'}
|
||||
</code>{' '}
|
||||
- <Trans>The signer's email</Trans>
|
||||
<code className="rounded bg-muted-foreground/20 p-1 text-muted-foreground text-sm">{'{signer.email}'}</code> -{' '}
|
||||
<Trans>The signer's email</Trans>
|
||||
</li>
|
||||
<li className="text-muted-foreground">
|
||||
<code className="text-muted-foreground bg-muted-foreground/20 rounded p-1 text-sm">
|
||||
{'{document.name}'}
|
||||
</code>{' '}
|
||||
<code className="rounded bg-muted-foreground/20 p-1 text-muted-foreground text-sm">{'{document.name}'}</code>{' '}
|
||||
- <Trans>The document's name</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Copy, Sparkles } from 'lucide-react';
|
||||
import type React from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
|
||||
import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '../../primitives/button';
|
||||
import {
|
||||
@@ -30,12 +30,7 @@ export type DocumentShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
trigger?: (_props: { loading: boolean; disabled: boolean }) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const DocumentShareButton = ({
|
||||
token,
|
||||
documentId,
|
||||
className,
|
||||
trigger,
|
||||
}: DocumentShareButtonProps) => {
|
||||
export const DocumentShareButton = ({ token, documentId, className, trigger }: DocumentShareButtonProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -60,7 +55,9 @@ export const DocumentShareButton = ({
|
||||
mutateAsync: createOrGetShareLink,
|
||||
data: shareLink,
|
||||
isPending: isCreatingOrGettingShareLink,
|
||||
} = trpc.document.share.useMutation();
|
||||
} = trpc.document.share.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
});
|
||||
|
||||
const isLoading = isCreatingOrGettingShareLink || isCopyingShareLink;
|
||||
|
||||
@@ -138,15 +135,14 @@ export const DocumentShareButton = ({
|
||||
|
||||
<DialogContent position="end">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<DialogTitle className="w-full max-w-full whitespace-pre-line break-words">
|
||||
<Trans>Share your signing experience!</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
<Trans>
|
||||
Rest assured, your document is strictly confidential and will never be shared. Only
|
||||
your signing experience will be highlighted. Share your personalized signing card to
|
||||
showcase your signature!
|
||||
Rest assured, your document is strictly confidential and will never be shared. Only your signing
|
||||
experience will be highlighted. Share your personalized signing card to showcase your signature!
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -154,8 +150,7 @@ export const DocumentShareButton = ({
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="rounded-md border p-4">
|
||||
I just {token ? 'signed' : 'sent'} a document in style with{' '}
|
||||
<span className="font-medium text-blue-400">@documenso</span>
|
||||
. Check it out!
|
||||
<span className="font-medium text-blue-400">@documenso</span>. Check it out!
|
||||
<span className="mt-2 block" />
|
||||
<span
|
||||
className={cn('break-all font-medium text-blue-400', {
|
||||
@@ -165,12 +160,9 @@ export const DocumentShareButton = ({
|
||||
{NEXT_PUBLIC_WEBAPP_URL()}/share/{shareLink?.slug || '...'}
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-muted/40 mt-4 aspect-[1200/630] overflow-hidden rounded-lg border',
|
||||
{
|
||||
'animate-pulse': !shareLink?.slug,
|
||||
},
|
||||
)}
|
||||
className={cn('mt-4 aspect-[1200/630] overflow-hidden rounded-lg border bg-muted/40', {
|
||||
'animate-pulse': !shareLink?.slug,
|
||||
})}
|
||||
>
|
||||
{shareLink?.slug && (
|
||||
<img
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export const DocumentSignatureSettingsTooltip = () => {
|
||||
return (
|
||||
<Tooltip>
|
||||
@@ -10,7 +9,7 @@ export const DocumentSignatureSettingsTooltip = () => {
|
||||
<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>Signature types</Trans>
|
||||
@@ -18,9 +17,7 @@ export const DocumentSignatureSettingsTooltip = () => {
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
The types of signatures that recipients are allowed to use when signing the document.
|
||||
</Trans>
|
||||
<Trans>The types of signatures that recipients are allowed to use when signing the document.</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { DOCUMENT_VISIBILITY } from '@documenso/lib/constants/document-visibility';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_VISIBILITY } from '@documenso/lib/constants/document-visibility';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export type DocumentVisibilitySelectType = SelectProps & {
|
||||
currentTeamMemberRole?: string;
|
||||
@@ -26,10 +18,7 @@ export type DocumentVisibilitySelectType = SelectProps & {
|
||||
};
|
||||
|
||||
export const DocumentVisibilitySelect = forwardRef<HTMLButtonElement, DocumentVisibilitySelectType>(
|
||||
(
|
||||
{ currentTeamMemberRole, isTeamSettings = false, disabled, canUpdateVisibility, ...props },
|
||||
ref,
|
||||
) => {
|
||||
({ currentTeamMemberRole, isTeamSettings = false, disabled, canUpdateVisibility, ...props }, ref) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const isAdmin = currentTeamMemberRole === TeamMemberRole.ADMIN;
|
||||
@@ -43,13 +32,8 @@ export const DocumentVisibilitySelect = forwardRef<HTMLButtonElement, DocumentVi
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value={DocumentVisibility.EVERYONE}>
|
||||
{_(DOCUMENT_VISIBILITY.EVERYONE.value)}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value={DocumentVisibility.MANAGER_AND_ABOVE}
|
||||
disabled={!isAdmin && !isManager}
|
||||
>
|
||||
<SelectItem value={DocumentVisibility.EVERYONE}>{_(DOCUMENT_VISIBILITY.EVERYONE.value)}</SelectItem>
|
||||
<SelectItem value={DocumentVisibility.MANAGER_AND_ABOVE} disabled={!isAdmin && !isManager}>
|
||||
{_(DOCUMENT_VISIBILITY.MANAGER_AND_ABOVE.value)}
|
||||
</SelectItem>
|
||||
<SelectItem value={DocumentVisibility.ADMIN} disabled={!isAdmin}>
|
||||
@@ -70,7 +54,7 @@ export const DocumentVisibilityTooltip = () => {
|
||||
<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 visibility</Trans>
|
||||
@@ -89,8 +73,7 @@ export const DocumentVisibilityTooltip = () => {
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Managers and above</strong> - Only managers and above can access and view the
|
||||
document
|
||||
<strong>Managers and above</strong> - Only managers and above can access and view the document
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { ClockIcon, EyeOffIcon, LockIcon } from 'lucide-react';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import { ClockIcon, EyeOffIcon, LockIcon } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { isTemplateRecipientEmailPlaceholder } from '../../../lib/constants/template';
|
||||
import { extractInitials } from '../../../lib/utils/recipient-formatter';
|
||||
@@ -20,15 +18,7 @@ import { PopoverHover } from '../../primitives/popover';
|
||||
interface EnvelopeRecipientFieldTooltipProps {
|
||||
field: Pick<
|
||||
Field,
|
||||
| 'id'
|
||||
| 'inserted'
|
||||
| 'positionX'
|
||||
| 'positionY'
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'page'
|
||||
| 'type'
|
||||
| 'fieldMeta'
|
||||
'id' | 'inserted' | 'positionX' | 'positionY' | 'width' | 'height' | 'page' | 'type' | 'fieldMeta'
|
||||
> & {
|
||||
recipient: Pick<Recipient, 'name' | 'email' | 'signingStatus'>;
|
||||
};
|
||||
@@ -70,9 +60,7 @@ export function EnvelopeRecipientFieldTooltip({
|
||||
});
|
||||
|
||||
const calculateCoords = useCallback(() => {
|
||||
const $page = document.querySelector<HTMLElement>(
|
||||
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
|
||||
);
|
||||
const $page = document.querySelector<HTMLElement>(`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`);
|
||||
|
||||
if (!$page) {
|
||||
return;
|
||||
@@ -111,9 +99,7 @@ export function EnvelopeRecipientFieldTooltip({
|
||||
}, [calculateCoords]);
|
||||
|
||||
useEffect(() => {
|
||||
const $page = document.querySelector<HTMLElement>(
|
||||
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
|
||||
);
|
||||
const $page = document.querySelector<HTMLElement>(`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`);
|
||||
|
||||
if (!$page) {
|
||||
return;
|
||||
@@ -145,8 +131,8 @@ export function EnvelopeRecipientFieldTooltip({
|
||||
>
|
||||
<PopoverHover
|
||||
trigger={
|
||||
<Avatar className="absolute -left-3 -top-3 z-50 h-6 w-6 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
|
||||
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
|
||||
<Avatar className="absolute -top-3 -left-3 z-50 h-6 w-6 border-2 border-gray-200/50 border-solid transition-colors hover:border-gray-200">
|
||||
<AvatarFallback className="bg-neutral-50 text-gray-400 text-xs">
|
||||
{extractInitials(field.recipient.name || field.recipient.email)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -186,15 +172,15 @@ export function EnvelopeRecipientFieldTooltip({
|
||||
)}
|
||||
|
||||
<p className="text-center font-semibold">
|
||||
<span>{t(FRIENDLY_FIELD_TYPE[field.type])} field</span>
|
||||
<span>
|
||||
<Trans>{t(FRIENDLY_FIELD_TYPE[field.type])} field</Trans>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-1 text-center text-xs">
|
||||
{getRecipientDisplayText(field.recipient)}
|
||||
</p>
|
||||
<p className="mt-1 text-center text-muted-foreground text-xs">{getRecipientDisplayText(field.recipient)}</p>
|
||||
|
||||
<button
|
||||
className="absolute right-0 top-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
|
||||
className="absolute top-0 right-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
|
||||
onClick={() => setHideField(true)}
|
||||
title="Hide field"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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';
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
|
||||
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 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" data-testid="envelope-expiration-mode">
|
||||
<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"
|
||||
data-testid="envelope-expiration-amount"
|
||||
value={amount}
|
||||
onChange={(e) => onAmountChange(Number(e.target.value))}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Select value={unit} onValueChange={onUnitChange} disabled={disabled}>
|
||||
<SelectTrigger className="flex-1 bg-background" data-testid="envelope-expiration-unit">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="day">
|
||||
<Plural value={amount} one="Day" other="Days" />
|
||||
</SelectItem>
|
||||
<SelectItem value="week">
|
||||
<Plural value={amount} one="Week" other="Weeks" />
|
||||
</SelectItem>
|
||||
<SelectItem value="month">
|
||||
<Plural value={amount} one="Month" other="Months" />
|
||||
</SelectItem>
|
||||
<SelectItem value="year">
|
||||
<Plural value={amount} one="Year" other="Years" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,253 @@
|
||||
import type {
|
||||
TEnvelopeReminderDurationPeriod,
|
||||
TEnvelopeReminderPeriod,
|
||||
TEnvelopeReminderSettings,
|
||||
} from '@documenso/lib/constants/envelope-reminder';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
|
||||
type ReminderMode = 'enabled' | 'disabled' | 'inherit';
|
||||
|
||||
const getMode = (value: TEnvelopeReminderSettings | null | undefined): ReminderMode => {
|
||||
if (value === null || value === undefined) {
|
||||
return 'inherit';
|
||||
}
|
||||
|
||||
if ('disabled' in value.sendAfter) {
|
||||
return 'disabled';
|
||||
}
|
||||
|
||||
return 'enabled';
|
||||
};
|
||||
|
||||
const getPeriodAmount = (period: TEnvelopeReminderPeriod | undefined): number => {
|
||||
if (period && 'amount' in period) {
|
||||
return period.amount;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getPeriodUnit = (period: TEnvelopeReminderPeriod | undefined): TEnvelopeReminderDurationPeriod['unit'] => {
|
||||
if (period && 'unit' in period) {
|
||||
return period.unit;
|
||||
}
|
||||
|
||||
return 'day';
|
||||
};
|
||||
|
||||
export type ReminderSettingsPickerProps = {
|
||||
value: TEnvelopeReminderSettings | null | undefined;
|
||||
onChange: (value: TEnvelopeReminderSettings | null) => void;
|
||||
disabled?: boolean;
|
||||
inheritLabel?: string;
|
||||
};
|
||||
|
||||
export const ReminderSettingsPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
inheritLabel,
|
||||
}: ReminderSettingsPickerProps) => {
|
||||
const mode = getMode(value);
|
||||
|
||||
const sendAfterAmount = getPeriodAmount(value?.sendAfter);
|
||||
const sendAfterUnit = getPeriodUnit(value?.sendAfter);
|
||||
const repeatEveryAmount = getPeriodAmount(value?.repeatEvery);
|
||||
const repeatEveryUnit = getPeriodUnit(value?.repeatEvery);
|
||||
|
||||
const onModeChange = (newMode: string) => {
|
||||
if (newMode === 'inherit') {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMode === 'disabled') {
|
||||
onChange({
|
||||
sendAfter: { disabled: true },
|
||||
repeatEvery: { disabled: true },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({
|
||||
sendAfter: { unit: sendAfterUnit, amount: sendAfterAmount },
|
||||
repeatEvery: { unit: repeatEveryUnit, amount: repeatEveryAmount },
|
||||
});
|
||||
};
|
||||
|
||||
const updateSendAfter = (updates: Partial<{ amount: number; unit: TEnvelopeReminderDurationPeriod['unit'] }>) => {
|
||||
const newAmount = Math.max(1, Math.floor(updates.amount ?? sendAfterAmount));
|
||||
const newUnit = updates.unit ?? sendAfterUnit;
|
||||
|
||||
onChange({
|
||||
sendAfter: { unit: newUnit, amount: newAmount },
|
||||
repeatEvery: value?.repeatEvery ?? { unit: repeatEveryUnit, amount: repeatEveryAmount },
|
||||
});
|
||||
};
|
||||
|
||||
const updateRepeatEvery = (updates: Partial<{ amount: number; unit: TEnvelopeReminderDurationPeriod['unit'] }>) => {
|
||||
const newAmount = Math.max(1, Math.floor(updates.amount ?? repeatEveryAmount));
|
||||
const newUnit = updates.unit ?? repeatEveryUnit;
|
||||
|
||||
onChange({
|
||||
sendAfter: value?.sendAfter ?? { unit: sendAfterUnit, amount: sendAfterAmount },
|
||||
repeatEvery: { unit: newUnit, amount: newAmount },
|
||||
});
|
||||
};
|
||||
|
||||
const onRepeatModeChange = (newMode: string) => {
|
||||
if (newMode === 'disabled') {
|
||||
onChange({
|
||||
sendAfter: value?.sendAfter ?? { unit: sendAfterUnit, amount: sendAfterAmount },
|
||||
repeatEvery: { disabled: true },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({
|
||||
sendAfter: value?.sendAfter ?? { unit: sendAfterUnit, amount: sendAfterAmount },
|
||||
repeatEvery: { unit: repeatEveryUnit, amount: repeatEveryAmount },
|
||||
});
|
||||
};
|
||||
|
||||
const repeatMode = value?.repeatEvery && 'disabled' in value.repeatEvery ? 'disabled' : 'enabled';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4" data-testid="reminder-settings-picker">
|
||||
<Select value={mode} onValueChange={onModeChange} disabled={disabled}>
|
||||
<SelectTrigger className="bg-background" data-testid="reminder-mode-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="enabled">
|
||||
<Trans>Enabled</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="disabled">
|
||||
<Trans>No reminders</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{inheritLabel !== undefined && <SelectItem value="inherit">{inheritLabel}</SelectItem>}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{mode === 'enabled' && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-muted-foreground text-sm">
|
||||
<Trans>Send first reminder after</Trans>
|
||||
</Label>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-20 bg-background"
|
||||
value={sendAfterAmount}
|
||||
onChange={(e) => updateSendAfter({ amount: Number(e.target.value) })}
|
||||
disabled={disabled}
|
||||
data-testid="reminder-send-after-amount"
|
||||
/>
|
||||
|
||||
<UnitSelect
|
||||
value={sendAfterUnit}
|
||||
amount={sendAfterAmount}
|
||||
onChange={(unit) =>
|
||||
updateSendAfter({
|
||||
unit: unit as TEnvelopeReminderDurationPeriod['unit'],
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
testId="reminder-send-after-unit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-muted-foreground text-sm">
|
||||
<Trans>Then repeat every</Trans>
|
||||
</Label>
|
||||
|
||||
<Select value={repeatMode} onValueChange={onRepeatModeChange} disabled={disabled}>
|
||||
<SelectTrigger className="bg-background" data-testid="reminder-repeat-mode-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="enabled">
|
||||
<Trans>Custom interval</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="disabled">
|
||||
<Trans>Don't repeat</Trans>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{repeatMode === 'enabled' && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-20 bg-background"
|
||||
value={repeatEveryAmount}
|
||||
onChange={(e) => updateRepeatEvery({ amount: Number(e.target.value) })}
|
||||
disabled={disabled}
|
||||
data-testid="reminder-repeat-amount"
|
||||
/>
|
||||
|
||||
<UnitSelect
|
||||
value={repeatEveryUnit}
|
||||
amount={repeatEveryAmount}
|
||||
onChange={(unit) =>
|
||||
updateRepeatEvery({
|
||||
unit: unit as TEnvelopeReminderDurationPeriod['unit'],
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
testId="reminder-repeat-unit"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UnitSelect = ({
|
||||
value,
|
||||
amount,
|
||||
onChange,
|
||||
disabled,
|
||||
testId,
|
||||
}: {
|
||||
value: string;
|
||||
amount: number;
|
||||
onChange: (value: string) => void;
|
||||
disabled: boolean;
|
||||
testId: string;
|
||||
}) => (
|
||||
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
||||
<SelectTrigger className="flex-1 bg-background" data-testid={testId}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="day">
|
||||
<Plural value={amount} one="Day" other="Days" />
|
||||
</SelectItem>
|
||||
<SelectItem value="week">
|
||||
<Plural value={amount} one="Week" other="Weeks" />
|
||||
</SelectItem>
|
||||
<SelectItem value="month">
|
||||
<Plural value={amount} one="Month" other="Months" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
Reference in New Issue
Block a user