import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react/macro'; import { Trans } 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 { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration'; 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 { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; 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 { ExpirationPeriodPicker } from '@documenso/ui/components/document/expiration-period-picker'; 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, }), envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(), }), }); 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 } = useLingui(); const { toast } = useToast(); const { envelope, updateEnvelopeAsync, editorConfig, isEmbedded, organisationEmails } = useCurrentEnvelopeEditor(); const { settings } = editorConfig; 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), envelopeExpirationPeriod: envelope.documentMeta?.envelopeExpirationPeriod ?? null, }, }; }; 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, }, { ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, enabled: Boolean(organisationEmails !== undefined && organisation.id), }, ); const emails = emailData?.data || organisationEmails || []; const canUpdateVisibility = canAccessTeamDocument(team.currentTeamRole, envelope.visibility); const onFormSubmit = async (data: TAddSettingsFormSchema) => { const { timezone, dateFormat, redirectUrl, language, signatureTypes, distributionMethod, emailId, emailSettings, message, subject, emailReplyTo, envelopeExpirationPeriod, } = 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, emailId, message, subject, emailReplyTo, emailSettings, distributionMethod, language: isValidLanguageCode(language) ? language : undefined, drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW), typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE), uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD), envelopeExpirationPeriod, }, }); setOpen(false); if (!isEmbedded) { 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 || !settings) { return null; } return ( !form.formState.isSubmitting && setOpen(value)} > e.stopPropagation()} asChild={true}> {trigger ?? ( )} {/* Sidebar. */}
Document Settings
{/* Content. */}
{selectedTab ? t(selectedTab.title) : ''} {selectedTab ? t(selectedTab.description) : ''}
{match({ activeTab, settings }) .with({ activeTab: 'general' }, () => ( <> {settings.allowConfigureLanguage && ( ( 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. )} /> )} {settings.allowConfigureSignatureTypes && ( ( Allowed Signature Types ({ label: t(option.label), value: option.value, }), )} selectedValues={field.value} onChange={field.onChange} className="w-full bg-background" emptySelectionPlaceholder="Select signature types" /> )} /> )} {settings.allowConfigureDateFormat && ( ( Date Format )} /> )} {settings.allowConfigureTimezone && ( ( 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 )} /> {settings.allowConfigureDistribution && ( ( 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.
)} /> )} {settings.allowConfigureExpirationPeriod && ( ( Expiration How long recipients have to complete this document after it is sent. Uses the team default when set to inherit. )} /> )} )) .with( { activeTab: 'email', settings: { allowConfigureDistribution: true } }, () => ( <> {settings.allowConfigureEmailSender && organisation.organisationClaim.flags.emailDomains && ( ( Email Sender )} /> )} {settings.allowConfigureEmailReplyTo && ( ( Reply To Email{' '} (Optional) )} /> )} ( Subject (Optional) )} /> ( Message (Optional)