Files
documenso/packages/ui/primitives/template-flow/add-template-settings.tsx
David Nguyen 193325717d fix: rework fields (#1697)
Rework:
- Field styling to improve visibility
- Field insertions, better alignment, centering and overflows

## Changes

General changes:

- Set default text alignment to left if no meta found
- Reduce borders and rings around fields to allow smaller fields
- Removed lots of redundant duplicated code surrounding field rendering
- Make fields more consistent across viewing, editing and signing
- Add more transparency to fields to allow users to see under fields
- No more optional/required/etc colors when signing, required fields
will be highlighted as orange when form is "validating"

Highlighted internal changes:

- Utilize native PDF fields to insert text, instead of drawing text 
- Change font auto scaling to only apply to when the height overflows
AND no custom font is set

⚠️ Multiline changes:

Multi line is enabled for a field under these conditions

1. Field content exceeds field width
2. Field includes a new line
3. Field type is TEXT

## [BEFORE] Field UI Signing 


![image](https://github.com/user-attachments/assets/ea002743-faeb-477c-a239-3ed240b54f55)

## [AFTER] Field UI Signing 


![image](https://github.com/user-attachments/assets/0f8eb252-4cf3-4d96-8d4f-cd085881b78c)

## [BEFORE] Signing a checkbox


![image](https://github.com/user-attachments/assets/4567d745-e1da-4202-a758-5d3c178c930e)

![image](https://github.com/user-attachments/assets/c25068e7-fe80-40f5-b63a-e8a0d4b38b6c)

## [AFTER] Signing a checkbox


![image](https://github.com/user-attachments/assets/effa5e3d-385a-4c35-bc8a-405386dd27d6)

![image](https://github.com/user-attachments/assets/64be34a9-0b32-424d-9264-15361c03eca5)

## [BEFORE] What a 2nd recipient sees once someone else signed a
document


![image](https://github.com/user-attachments/assets/21c21ae2-fc62-4ccc-880a-46aab012aa70)

## [AFTER] What a 2nd recipient sees once someone else signed a document


![image](https://github.com/user-attachments/assets/ae51677b-f1d5-4008-a7fd-756533166542)

## **[BEFORE]** Inserting fields


![image](https://github.com/user-attachments/assets/1a8bb8da-9a15-4deb-bc28-eb349414465c)

## **[AFTER]** Inserting fields


![image](https://github.com/user-attachments/assets/c52c5238-9836-45aa-b8a4-bc24a3462f40)

## Overflows, multilines and field alignments testing

Debugging borders:
- Red border = The original field placement without any modifications
- Blue border = The available space to overflow

### Single line overflows and field alignments 

This is left aligned fields, overflow will always go to the end of the
page and will not wrap


![image](https://github.com/user-attachments/assets/47003658-783e-4f9c-adbf-c4686804d98f)

This is center aligned fields, the max width is the closest edge to the
page * 2


![image](https://github.com/user-attachments/assets/05a38093-75d6-4600-bae2-21ecff63e115)

This is right aligned text, the width will extend all the way to the
left hand side of the page


![image](https://github.com/user-attachments/assets/6a9d84a8-4166-4626-9fb3-1577fac2571e)

### Multiline line overflows and field alignments 

These are text fields that can be overflowed


![image](https://github.com/user-attachments/assets/f7b5456e-2c49-48b2-8d4c-ab1dc3401644)

Another example of left aligned text overflows with more text


![image](https://github.com/user-attachments/assets/3db6b35e-4c8d-4ffe-8036-0da760d9c167)
2025-04-23 21:40:42 +10:00

590 lines
23 KiB
TypeScript

import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
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 } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
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 {
DocumentVisibilitySelect,
DocumentVisibilityTooltip,
} from '@documenso/ui/components/document/document-visibility-select';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@documenso/ui/primitives/accordion';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '../../components/document/document-read-only-fields';
import { DocumentSignatureSettingsTooltip } from '../../components/document/document-signature-settings-tooltip';
import { Combobox } from '../combobox';
import {
DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter,
DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep,
} from '../document-flow/document-flow-root';
import type { DocumentFlowStep } from '../document-flow/types';
import { Input } from '../input';
import { MultiSelectCombobox } from '../multi-select-combobox';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
import { useStep } from '../stepper';
import { Textarea } from '../textarea';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
import type { TAddTemplateSettingsFormSchema } from './add-template-settings.types';
import { ZAddTemplateSettingsFormSchema } from './add-template-settings.types';
export type AddTemplateSettingsFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
fields: Field[];
isEnterprise: boolean;
isDocumentPdfLoaded: boolean;
template: TTemplate;
currentTeamMemberRole?: TeamMemberRole;
onSubmit: (_data: TAddTemplateSettingsFormSchema) => void;
};
export const AddTemplateSettingsFormPartial = ({
documentFlow,
recipients,
fields,
isEnterprise,
isDocumentPdfLoaded,
template,
currentTeamMemberRole,
onSubmit,
}: AddTemplateSettingsFormProps) => {
const { t, i18n } = useLingui();
const { documentAuthOption } = extractDocumentAuthMethods({
documentAuth: template.authOptions,
});
const form = useForm<TAddTemplateSettingsFormSchema>({
resolver: zodResolver(ZAddTemplateSettingsFormSchema),
defaultValues: {
title: template.title,
externalId: template.externalId || undefined,
visibility: template.visibility || '',
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
meta: {
subject: template.templateMeta?.subject ?? '',
message: template.templateMeta?.message ?? '',
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
dateFormat: (template.templateMeta?.dateFormat ??
DEFAULT_DOCUMENT_DATE_FORMAT) as TDocumentMetaDateFormat,
distributionMethod:
template.templateMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
redirectUrl: template.templateMeta?.redirectUrl ?? '',
language: template.templateMeta?.language ?? 'en',
emailSettings: ZDocumentEmailSettingsSchema.parse(template?.templateMeta?.emailSettings),
signatureTypes: extractTeamSignatureSettings(template?.templateMeta),
},
},
});
const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
const distributionMethod = form.watch('meta.distributionMethod');
const emailSettings = form.watch('meta.emailSettings');
const canUpdateVisibility = match(currentTeamMemberRole)
.with(TeamMemberRole.ADMIN, () => true)
.with(
TeamMemberRole.MANAGER,
() =>
template.visibility === DocumentVisibility.EVERYONE ||
template.visibility === DocumentVisibility.MANAGER_AND_ABOVE,
)
.otherwise(() => false);
// We almost always want to set the timezone to the user's local timezone to avoid confusion
// when the document is signed.
useEffect(() => {
if (!form.formState.touchedFields.meta?.timezone && !template.templateMeta?.timezone) {
form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
}
}, [form, form.setValue, form.formState.touchedFields.meta?.timezone]);
return (
<>
<DocumentFlowFormContainerHeader
title={documentFlow.title}
description={documentFlow.description}
/>
<DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && (
<DocumentReadOnlyFields
showRecipientColors={true}
recipientIds={recipients.map((recipient) => recipient.id)}
fields={mapFieldsWithRecipients(fields, recipients)}
/>
)}
<Form {...form}>
<fieldset
className="flex h-full flex-col space-y-6"
disabled={form.formState.isSubmitting}
>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Template title</Trans>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="meta.language"
render={({ field }) => (
<FormItem>
<FormLabel className="inline-flex items-center">
<Trans>Language</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
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.
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => (
<SelectItem key={code} value={code}>
{language.full}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="globalAccessAuth"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Document access</Trans>
<DocumentGlobalAuthAccessTooltip />
</FormLabel>
<FormControl>
<DocumentGlobalAuthAccessSelect {...field} onValueChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
{currentTeamMemberRole && (
<FormField
control={form.control}
name="visibility"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
Document visibility
<DocumentVisibilityTooltip />
</FormLabel>
<FormControl>
<DocumentVisibilitySelect
canUpdateVisibility={canUpdateVisibility}
currentTeamMemberRole={currentTeamMemberRole}
{...field}
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="meta.distributionMethod"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Document Distribution Method</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
<h2>
<strong>
<Trans>Document Distribution Method</Trans>
</strong>
</h2>
<p>
<Trans>
This is how the document will reach the recipients once the document is
ready for signing.
</Trans>
</p>
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
<li>
<Trans>
<strong>Email</strong> - The recipient will be emailed the document to
sign, approve, etc.
</Trans>
</li>
<li>
<Trans>
<strong>None</strong> - We will generate links which you can send to
the recipients manually.
</Trans>
</li>
</ul>
<Trans>
<strong>Note</strong> - If you use Links in combination with direct
templates, you will need to manually send the links to the remaining
recipients.
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="bg-background text-muted-foreground">
<SelectValue data-testid="documentDistributionMethodSelectValue" />
</SelectTrigger>
<SelectContent position="popper">
{Object.values(DOCUMENT_DISTRIBUTION_METHODS).map(
({ value, description }) => (
<SelectItem key={value} value={value}>
{i18n._(description)}
</SelectItem>
),
)}
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="meta.signatureTypes"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Allowed Signature Types</Trans>
<DocumentSignatureSettingsTooltip />
</FormLabel>
<FormControl>
<MultiSelectCombobox
options={Object.values(DOCUMENT_SIGNATURE_TYPES).map((option) => ({
label: t(option.label),
value: option.value,
}))}
selectedValues={field.value}
onChange={field.onChange}
className="bg-background w-full"
emptySelectionPlaceholder="Select signature types"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isEnterprise && (
<FormField
control={form.control}
name="globalActionAuth"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Recipient action authentication</Trans>
<DocumentGlobalAuthActionTooltip />
</FormLabel>
<FormControl>
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
)}
{distributionMethod === DocumentDistributionMethod.EMAIL && (
<Accordion type="multiple">
<AccordionItem value="email-options" className="border-none">
<AccordionTrigger className="text-foreground rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
<Trans>Email Options</Trans>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-4 text-sm leading-relaxed [&>div]:pb-0">
<div className="flex flex-col space-y-6">
<FormField
control={form.control}
name="meta.subject"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>
Subject <span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="meta.message"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>
Message <span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
<FormControl>
<Textarea className="bg-background h-32 resize-none" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DocumentSendEmailMessageHelper />
<DocumentEmailCheckboxes
value={emailSettings}
onChange={(value) => form.setValue('meta.emailSettings', value)}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
<Accordion type="multiple">
<AccordionItem value="advanced-options" className="border-none">
<AccordionTrigger className="text-foreground rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
<Trans>Advanced Options</Trans>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-4 text-sm leading-relaxed">
<div className="flex flex-col space-y-6">
<FormField
control={form.control}
name="externalId"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>External ID</Trans>{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<Trans>
Add an external ID to the template. This can be used to identify
in external systems.
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="meta.dateFormat"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Date Format</Trans>
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
{DATE_FORMATS.map((format) => (
<SelectItem key={format.key} value={format.value}>
{format.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="meta.timezone"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Time Zone</Trans>
</FormLabel>
<FormControl>
<Combobox
className="bg-background time-zone-field"
options={TIME_ZONES}
{...field}
onChange={(value) => value && field.onChange(value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="meta.redirectUrl"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Redirect URL</Trans>{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<Trans>
Add a URL to redirect the user to once the document is signed
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</fieldset>
</Form>
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={form.formState.isSubmitting}
disabled={form.formState.isSubmitting}
canGoBack={stepIndex !== 0}
onGoBackClick={previousStep}
onGoNextClick={form.handleSubmit(onSubmit)}
/>
</DocumentFlowFormContainerFooter>
</>
);
};