import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { DocumentDistributionMethod, DocumentVisibility, EnvelopeType, SendStatus, } from '@prisma/client'; import type * as DialogPrimitive from '@radix-ui/react-dialog'; import { InfoIcon, MailIcon, SettingsIcon, ShieldIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { z } from 'zod'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DOCUMENT_DISTRIBUTION_METHODS, DOCUMENT_SIGNATURE_TYPES, } from '@documenso/lib/constants/document'; import { SUPPORTED_LANGUAGES, SUPPORTED_LANGUAGE_CODES, isValidLanguageCode, } from '@documenso/lib/constants/i18n'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { AppError } from '@documenso/lib/errors/app-error'; import { ZDocumentAccessAuthTypesSchema, ZDocumentActionAuthTypesSchema, } from '@documenso/lib/types/document-auth'; import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; import { type TDocumentMetaDateFormat, ZDocumentMetaDateFormatSchema, ZDocumentMetaTimezoneSchema, } from '@documenso/lib/types/document-meta'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url'; import { DocumentSignatureType, canAccessTeamDocument, extractTeamSignatureSettings, } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; import { DocumentEmailCheckboxes } from '@documenso/ui/components/document/document-email-checkboxes'; import { DocumentGlobalAuthAccessSelect, DocumentGlobalAuthAccessTooltip, } from '@documenso/ui/components/document/document-global-auth-access-select'; import { DocumentGlobalAuthActionSelect, DocumentGlobalAuthActionTooltip, } from '@documenso/ui/components/document/document-global-auth-action-select'; import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper'; import { DocumentSignatureSettingsTooltip } from '@documenso/ui/components/document/document-signature-settings-tooltip'; import { DocumentVisibilitySelect, DocumentVisibilityTooltip, } from '@documenso/ui/components/document/document-visibility-select'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { CardDescription, CardHeader, CardTitle } from '@documenso/ui/primitives/card'; import { Combobox } from '@documenso/ui/primitives/combobox'; import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from '@documenso/ui/primitives/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@documenso/ui/primitives/select'; import { Textarea } from '@documenso/ui/primitives/textarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useCurrentTeam } from '~/providers/team'; export const ZAddSettingsFormSchema = z.object({ externalId: z.string().optional(), visibility: z.nativeEnum(DocumentVisibility).optional(), globalAccessAuth: z .array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')])) .transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val)) .optional() .default([]), globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]), meta: z.object({ subject: z.string(), message: z.string(), timezone: ZDocumentMetaTimezoneSchema.default(DEFAULT_DOCUMENT_TIME_ZONE), dateFormat: ZDocumentMetaDateFormatSchema.default(DEFAULT_DOCUMENT_DATE_FORMAT), distributionMethod: z .nativeEnum(DocumentDistributionMethod) .optional() .default(DocumentDistributionMethod.EMAIL), redirectUrl: z .string() .optional() .refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), { message: 'Please enter a valid URL, make sure you include http:// or https:// part of the url.', }), language: z .union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)]) .optional() .default('en'), emailId: z.string().nullable(), emailReplyTo: z.preprocess( (val) => (val === '' ? undefined : val), z.string().email().optional(), ), emailSettings: ZDocumentEmailSettingsSchema, signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, { message: msg`At least one signature type must be enabled`.id, }), }), }); type EnvelopeEditorSettingsTabType = 'general' | 'email' | 'security'; const tabs = [ { id: 'general', title: msg`General`, icon: SettingsIcon, description: msg`Configure document settings and options before sending.`, }, { id: 'email', title: msg`Email`, icon: MailIcon, description: msg`Configure email settings for the document`, }, { id: 'security', title: msg`Security`, icon: ShieldIcon, description: msg`Configure security settings for the document`, }, ] as const; type TAddSettingsFormSchema = z.infer; type EnvelopeEditorSettingsDialogProps = { trigger?: React.ReactNode; } & Omit; export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEditorSettingsDialogProps) => { const { t, i18n } = useLingui(); const { toast } = useToast(); const { envelope, updateEnvelopeAsync } = useCurrentEnvelopeEditor(); const team = useCurrentTeam(); const organisation = useCurrentOrganisation(); const [open, setOpen] = useState(false); const [activeTab, setActiveTab] = useState('general'); const { documentAuthOption } = extractDocumentAuthMethods({ documentAuth: envelope.authOptions, }); const createDefaultValues = () => { return { externalId: envelope.externalId || '', visibility: envelope.visibility || '', globalAccessAuth: documentAuthOption?.globalAccessAuth || [], globalActionAuth: documentAuthOption?.globalActionAuth || [], meta: { subject: envelope.documentMeta.subject ?? '', message: envelope.documentMeta.message ?? '', timezone: envelope.documentMeta.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, // eslint-disable-next-line @typescript-eslint/consistent-type-assertions dateFormat: (envelope.documentMeta.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT) as TDocumentMetaDateFormat, distributionMethod: envelope.documentMeta.distributionMethod || DocumentDistributionMethod.EMAIL, redirectUrl: envelope.documentMeta.redirectUrl ?? '', language: envelope.documentMeta.language ?? 'en', emailId: envelope.documentMeta.emailId ?? null, emailReplyTo: envelope.documentMeta.emailReplyTo ?? undefined, emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings), signatureTypes: extractTeamSignatureSettings(envelope.documentMeta), }, }; }; const form = useForm({ resolver: zodResolver(ZAddSettingsFormSchema), defaultValues: createDefaultValues(), }); const envelopeHasBeenSent = envelope.type === EnvelopeType.DOCUMENT && envelope.recipients.some((recipient) => recipient.sendStatus === SendStatus.SENT); const emailSettings = form.watch('meta.emailSettings'); const { data: emailData, isLoading: isLoadingEmails } = trpc.enterprise.organisation.email.find.useQuery({ organisationId: organisation.id, perPage: 100, }); const emails = emailData?.data || []; const canUpdateVisibility = canAccessTeamDocument(team.currentTeamRole, envelope.visibility); const onFormSubmit = async (data: TAddSettingsFormSchema) => { const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta; const parsedGlobalAccessAuth = z .array(ZDocumentAccessAuthTypesSchema) .safeParse(data.globalAccessAuth); try { await updateEnvelopeAsync({ data: { externalId: data.externalId || null, visibility: data.visibility, globalAccessAuth: parsedGlobalAccessAuth.success ? parsedGlobalAccessAuth.data : [], globalActionAuth: data.globalActionAuth ?? [], }, meta: { timezone, dateFormat, redirectUrl, language: isValidLanguageCode(language) ? language : undefined, typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE), uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD), drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW), }, }); setOpen(false); toast({ title: t`Success`, description: t`Envelope updated`, duration: 5000, }); } catch (err) { const error = AppError.parseError(err); console.error(error); toast({ title: t`An unknown error occurred`, description: t`We encountered an unknown error while attempting to update the envelope. Please try again later.`, variant: 'destructive', }); } }; useEffect(() => { if ( !form.formState.touchedFields.meta?.timezone && !envelopeHasBeenSent && !envelope.documentMeta.timezone ) { form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone); } }, [ envelopeHasBeenSent, form, form.setValue, form.formState.touchedFields.meta?.timezone, envelope.documentMeta.timezone, ]); useEffect(() => { form.reset(createDefaultValues()); setActiveTab('general'); }, [open, form]); const selectedTab = tabs.find((tab) => tab.id === activeTab); if (!selectedTab) { return null; } return ( !form.formState.isSubmitting && setOpen(value)} > e.stopPropagation()} asChild={true}> {trigger ?? ( )} {/* Sidebar. */}
Document Settings
{/* Content. */}
{t(selectedTab?.title ?? '')} {t(selectedTab?.description ?? '')}
{match(activeTab) .with('general', () => ( <> ( Language Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document. )} /> ( Allowed Signature Types ({ label: t(option.label), value: option.value, }))} selectedValues={field.value} onChange={field.onChange} className="bg-background w-full" emptySelectionPlaceholder="Select signature types" /> )} /> ( Date Format )} /> ( Time Zone value && field.onChange(value)} disabled={envelopeHasBeenSent} /> )} /> ( External ID{' '} Add an external ID to the document. This can be used to identify the document in external systems. )} /> ( Redirect URL{' '} Add a URL to redirect the user to once the document is signed )} /> ( Document Distribution Method

Document Distribution Method

This is how the document will reach the recipients once the document is ready for signing.

  • Email - The recipient will be emailed the document to sign, approve, etc.
  • None - We will generate links which you can send to the recipients manually.
Note - If you use Links in combination with direct templates, you will need to manually send the links to the remaining recipients.
)} /> )) .with('email', () => ( <> {organisation.organisationClaim.flags.emailDomains && ( ( Email Sender )} /> )} ( Reply To Email{' '} (Optional) )} /> ( Subject (Optional) )} /> ( Message{' '} (Optional)