chore: merge main

This commit is contained in:
Catalin Pit
2025-09-11 14:58:42 +03:00
343 changed files with 14952 additions and 3564 deletions

View File

@ -21,9 +21,11 @@ import { useFieldArray, useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import {
type TFieldMetaSchema as FieldMeta,
ZFieldMetaSchema,
@ -72,6 +74,7 @@ export type AddTemplateFieldsFormProps = {
recipients: Recipient[];
fields: Field[];
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
onAutoSave: (_data: TAddTemplateFieldsFormSchema) => Promise<void>;
teamId: number;
};
@ -80,6 +83,7 @@ export const AddTemplateFieldsFormPartial = ({
recipients,
fields,
onSubmit,
onAutoSave,
teamId,
}: AddTemplateFieldsFormProps) => {
const { _ } = useLingui();
@ -120,6 +124,20 @@ export const AddTemplateFieldsFormPartial = ({
const onFormSubmit = form.handleSubmit(onSubmit);
const { scheduleSave } = useAutoSave(onAutoSave);
const handleAutoSave = async () => {
const isFormValid = await form.trigger();
if (!isFormValid) {
return;
}
const formData = form.getValues();
scheduleSave(formData);
};
const {
append,
remove,
@ -159,6 +177,7 @@ export const AddTemplateFieldsFormPartial = ({
};
append(newField);
void handleAutoSave();
return;
}
@ -186,6 +205,7 @@ export const AddTemplateFieldsFormPartial = ({
append(newField);
});
void handleAutoSave();
return;
}
@ -197,7 +217,15 @@ export const AddTemplateFieldsFormPartial = ({
});
}
},
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
[
append,
lastActiveField,
selectedSigner?.email,
selectedSigner?.id,
selectedSigner?.token,
toast,
handleAutoSave,
],
);
const onFieldPaste = useCallback(
@ -217,9 +245,18 @@ export const AddTemplateFieldsFormPartial = ({
pageX: copiedField.pageX + 3,
pageY: copiedField.pageY + 3,
});
void handleAutoSave();
}
},
[append, fieldClipboard, selectedSigner?.email, selectedSigner?.id, selectedSigner?.token],
[
append,
fieldClipboard,
selectedSigner?.email,
selectedSigner?.id,
selectedSigner?.token,
handleAutoSave,
],
);
useHotkeys(['ctrl+c', 'meta+c'], (evt) => onFieldCopy(evt));
@ -377,8 +414,10 @@ export const AddTemplateFieldsFormPartial = ({
pageWidth,
pageHeight,
});
void handleAutoSave();
},
[getFieldPosition, localFields, update],
[getFieldPosition, localFields, update, handleAutoSave],
);
const onFieldMove = useCallback(
@ -400,8 +439,10 @@ export const AddTemplateFieldsFormPartial = ({
pageX,
pageY,
});
void handleAutoSave();
},
[getFieldPosition, localFields, update],
[getFieldPosition, localFields, update, handleAutoSave],
);
useEffect(() => {
@ -503,6 +544,7 @@ export const AddTemplateFieldsFormPartial = ({
});
form.setValue('fields', updatedFields);
void handleAutoSave();
};
return (
@ -518,6 +560,10 @@ export const AddTemplateFieldsFormPartial = ({
fields={localFields}
onAdvancedSettings={handleAdvancedSettings}
onSave={handleSavedFieldSettings}
onAutoSave={async (fieldState) => {
handleSavedFieldSettings(fieldState);
await handleAutoSave();
}}
/>
) : (
<>
@ -565,12 +611,22 @@ export const AddTemplateFieldsFormPartial = ({
defaultWidth={DEFAULT_WIDTH_PX}
passive={isFieldWithinBounds && !!selectedField}
onFocus={() => setLastActiveField(field)}
onBlur={() => setLastActiveField(null)}
onBlur={() => {
setLastActiveField(null);
void handleAutoSave();
}}
onResize={(options) => onFieldResize(options, index)}
onMove={(options) => onFieldMove(options, index)}
onRemove={() => remove(index)}
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
onRemove={() => {
remove(index);
void handleAutoSave();
}}
onDuplicate={() => {
onFieldCopy(null, { duplicate: true });
}}
onDuplicateAllPages={() => {
onFieldCopy(null, { duplicateAll: true });
}}
onAdvancedSettings={() => {
setCurrentField(field);
handleAdvancedSettings();
@ -593,15 +649,21 @@ export const AddTemplateFieldsFormPartial = ({
selectedSignerStyles?.comboxBoxTrigger,
)}
>
{selectedSigner?.email && (
<span className="flex-1 truncate text-left">
{selectedSigner?.name} ({selectedSigner?.email})
</span>
)}
{selectedSigner?.email &&
!isTemplateRecipientEmailPlaceholder(selectedSigner.email) && (
<span className="flex-1 truncate text-left">
{selectedSigner?.name} ({selectedSigner?.email})
</span>
)}
{selectedSigner?.email &&
isTemplateRecipientEmailPlaceholder(selectedSigner.email) && (
<span className="flex-1 truncate text-left">{selectedSigner?.name}</span>
)}
{!selectedSigner?.email && (
<span className="gradie flex-1 truncate text-left">
{selectedSigner?.email}
No recipient selected
</span>
)}
@ -657,15 +719,22 @@ export const AddTemplateFieldsFormPartial = ({
'text-foreground/80': recipient === selectedSigner,
})}
>
{recipient.name && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{recipient.name &&
!isTemplateRecipientEmailPlaceholder(recipient.email) && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && (
<span title={recipient.email}>{recipient.email}</span>
)}
{recipient.name &&
isTemplateRecipientEmailPlaceholder(recipient.email) && (
<span title={recipient.name}>{recipient.name}</span>
)}
{!recipient.name &&
!isTemplateRecipientEmailPlaceholder(recipient.email) && (
<span title={recipient.email}>{recipient.email}</span>
)}
</span>
</CommandItem>
))}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { useCallback, useId, useMemo, useRef, useState } from 'react';
import type { DropResult, SensorAPI } from '@hello-pangea/dnd';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
@ -12,8 +12,10 @@ import { motion } from 'framer-motion';
import { GripVerticalIcon, HelpCircle, Link2Icon, Plus, Trash } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
@ -54,6 +56,7 @@ export type AddTemplatePlaceholderRecipientsFormProps = {
allowDictateNextSigner?: boolean;
templateDirectLink?: TemplateDirectLink | null;
onSubmit: (_data: TAddTemplatePlacholderRecipientsFormSchema) => void;
onAutoSave: (_data: TAddTemplatePlacholderRecipientsFormSchema) => Promise<void>;
isDocumentPdfLoaded: boolean;
};
@ -66,6 +69,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
allowDictateNextSigner,
isDocumentPdfLoaded,
onSubmit,
onAutoSave,
}: AddTemplatePlaceholderRecipientsFormProps) => {
const initialId = useId();
const $sensorApi = useRef<SensorAPI | null>(null);
@ -122,15 +126,38 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
},
});
useEffect(() => {
form.reset({
signers: generateDefaultFormSigners(),
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
allowDictateNextSigner: allowDictateNextSigner ?? false,
});
const emptySigners = useCallback(
() => form.getValues('signers').filter((signer) => signer.email === ''),
[form],
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recipients]);
const { scheduleSave } = useAutoSave(onAutoSave);
const handleAutoSave = async () => {
if (emptySigners().length > 0) {
return;
}
const isFormValid = await form.trigger();
if (!isFormValid) {
return;
}
const formData = form.getValues();
scheduleSave(formData);
};
// useEffect(() => {
// form.reset({
// signers: generateDefaultFormSigners(),
// signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
// allowDictateNextSigner: allowDictateNextSigner ?? false,
// });
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [recipients]);
// Always show advanced settings if any recipient has auth options.
const alwaysShowAdvancedSettings = useMemo(() => {
@ -203,7 +230,12 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const onRemoveSigner = (index: number) => {
removeSigner(index);
const updatedSigners = signers.filter((_, idx) => idx !== index);
form.setValue('signers', normalizeSigningOrders(updatedSigners));
form.setValue('signers', normalizeSigningOrders(updatedSigners), {
shouldValidate: true,
shouldDirty: true,
});
void handleAutoSave();
};
const isSignerDirectRecipient = (
@ -230,7 +262,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signingOrder: index + 1,
}));
form.setValue('signers', updatedSigners);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
const lastSigner = updatedSigners[updatedSigners.length - 1];
if (lastSigner.role === RecipientRole.ASSISTANT) {
@ -243,64 +278,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
}
await form.trigger('signers');
void handleAutoSave();
},
[form, watchedSigners, toast],
);
const triggerDragAndDrop = useCallback(
(fromIndex: number, toIndex: number) => {
if (!$sensorApi.current) {
return;
}
const draggableId = signers[fromIndex].id;
const preDrag = $sensorApi.current.tryGetLock(draggableId);
if (!preDrag) {
return;
}
const drag = preDrag.snapLift();
setTimeout(() => {
// Move directly to the target index
if (fromIndex < toIndex) {
for (let i = fromIndex; i < toIndex; i++) {
drag.moveDown();
}
} else {
for (let i = fromIndex; i > toIndex; i--) {
drag.moveUp();
}
}
setTimeout(() => {
drag.drop();
}, 500);
}, 0);
},
[signers],
);
const updateSigningOrders = useCallback(
(newIndex: number, oldIndex: number) => {
const updatedSigners = form.getValues('signers').map((signer, index) => {
if (index === oldIndex) {
return { ...signer, signingOrder: newIndex + 1 };
} else if (index >= newIndex && index < oldIndex) {
return { ...signer, signingOrder: (signer.signingOrder ?? index + 1) + 1 };
} else if (index <= newIndex && index > oldIndex) {
return { ...signer, signingOrder: Math.max(1, (signer.signingOrder ?? index + 1) - 1) };
}
return signer;
});
updatedSigners.forEach((signer, index) => {
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
});
},
[form],
[form, watchedSigners, toast, handleAutoSave],
);
const handleSigningOrderChange = useCallback(
@ -328,7 +309,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signingOrder: idx + 1,
}));
form.setValue('signers', updatedSigners);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
toast({
@ -338,8 +322,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
),
});
}
void handleAutoSave();
},
[form, toast],
[form, toast, handleAutoSave],
);
const handleRoleChange = useCallback(
@ -349,7 +335,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
// Handle parallel to sequential conversion for assistants
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL, {
shouldValidate: true,
shouldDirty: true,
});
toast({
title: _(msg`Signing order is enabled.`),
description: _(msg`You cannot add assistants when signing order is disabled.`),
@ -364,7 +353,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signingOrder: idx + 1,
}));
form.setValue('signers', updatedSigners);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
toast({
@ -374,8 +366,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
),
});
}
void handleAutoSave();
},
[form, toast],
[form, toast, handleAutoSave],
);
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
@ -389,10 +383,21 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
}));
form.setValue('signers', updatedSigners);
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
form.setValue('allowDictateNextSigner', false);
}, [form]);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL, {
shouldValidate: true,
shouldDirty: true,
});
form.setValue('allowDictateNextSigner', false, {
shouldValidate: true,
shouldDirty: true,
});
void handleAutoSave();
}, [form, handleAutoSave]);
return (
<>
@ -437,8 +442,13 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
// If sequential signing is turned off, disable dictate next signer
if (!checked) {
form.setValue('allowDictateNextSigner', false);
form.setValue('allowDictateNextSigner', false, {
shouldValidate: true,
shouldDirty: true,
});
}
void handleAutoSave();
}}
disabled={isSubmitting}
/>
@ -464,7 +474,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
{...field}
id="allowDictateNextSigner"
checked={value}
onCheckedChange={field.onChange}
onCheckedChange={(checked) => {
field.onChange(checked);
void handleAutoSave();
}}
disabled={isSubmitting || !isSigningOrderSequential}
/>
</FormControl>
@ -555,6 +568,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<Input
type="number"
max={signers.length}
data-testid="placeholder-recipient-signing-order-input"
className={cn(
'w-full text-center',
'[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
@ -592,7 +606,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
})}
>
{!showAdvancedSettings && index === 0 && (
<FormLabel required>
<FormLabel>
<Trans>Email</Trans>
</FormLabel>
)}
@ -602,12 +616,20 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
type="email"
placeholder={_(msg`Email`)}
{...field}
value={
isTemplateRecipientEmailPlaceholder(field.value)
? ''
: field.value
}
disabled={
field.disabled ||
isSubmitting ||
signers[index].email === user?.email ||
isSignerDirectRecipient(signer)
}
maxLength={254}
onBlur={handleAutoSave}
data-testid="placeholder-recipient-email-input"
/>
</FormControl>
@ -642,6 +664,9 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signers[index].email === user?.email ||
isSignerDirectRecipient(signer)
}
maxLength={255}
onBlur={handleAutoSave}
data-testid="placeholder-recipient-name-input"
/>
</FormControl>
@ -683,10 +708,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<FormControl>
<RecipientRoleSelect
{...field}
onValueChange={(value) =>
onValueChange={(value) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
handleRoleChange(index, value as RecipientRole)
}
handleRoleChange(index, value as RecipientRole);
}}
disabled={isSubmitting}
hideCCRecipients={isSignerDirectRecipient(signer)}
/>
@ -722,6 +747,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting || signers.length === 1}
onClick={() => onRemoveSigner(index)}
data-testid="remove-placeholder-recipient-button"
>
<Trash className="h-5 w-5" />
</button>

View File

@ -1,6 +1,7 @@
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX } from '@documenso/lib/constants/template';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
export const ZAddTemplatePlacholderRecipientsFormSchema = z
@ -10,7 +11,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z.string().min(1).email(),
name: z.string(),
name: z.string().min(1, { message: 'Name is required' }),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
@ -21,12 +22,25 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
const nonPlaceholderEmails = schema.signers
.map((signer) => signer.email.toLowerCase())
.filter((email) => !TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX.test(email));
return new Set(emails).size === emails.length;
return new Set(nonPlaceholderEmails).size === nonPlaceholderEmails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
)
.refine(
/*
Since placeholder emails are empty, we need to check that the names are unique.
If we don't do this, the app will add duplicate signers and merge them in the next step, where you add fields.
*/
(schema) => {
const names = schema.signers.map((signer) => signer.name.trim());
return new Set(names).size === names.length;
},
{ message: 'Signers must have unique names', path: ['signers__root'] },
);
export type TAddTemplatePlacholderRecipientsFormSchema = z.infer<

View File

@ -9,6 +9,7 @@ import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import {
@ -83,6 +84,7 @@ export type AddTemplateSettingsFormProps = {
template: TTemplate;
currentTeamMemberRole?: TeamMemberRole;
onSubmit: (_data: TAddTemplateSettingsFormSchema) => void;
onAutoSave: (_data: TAddTemplateSettingsFormSchema) => Promise<void>;
};
export const AddTemplateSettingsFormPartial = ({
@ -93,6 +95,7 @@ export const AddTemplateSettingsFormPartial = ({
template,
currentTeamMemberRole,
onSubmit,
onAutoSave,
}: AddTemplateSettingsFormProps) => {
const { t, i18n } = useLingui();
@ -160,6 +163,28 @@ export const AddTemplateSettingsFormPartial = ({
}
}, [form, form.setValue, form.formState.touchedFields.meta?.timezone]);
const { scheduleSave } = useAutoSave(onAutoSave);
const handleAutoSave = async () => {
const isFormValid = await form.trigger();
if (!isFormValid) {
return;
}
const formData = form.getValues();
/*
* Parse the form data through the Zod schema to handle transformations
* (like -1 -> undefined for the Document Global Auth Access)
*/
const parseResult = ZAddTemplateSettingsFormSchema.safeParse(formData);
if (parseResult.success) {
scheduleSave(parseResult.data);
}
};
return (
<>
<DocumentFlowFormContainerHeader
@ -191,7 +216,12 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
<Input
className="bg-background"
{...field}
maxLength={255}
onBlur={handleAutoSave}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -219,7 +249,13 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<Select
{...field}
onValueChange={(value) => {
field.onChange(value);
void handleAutoSave();
}}
>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
@ -250,9 +286,13 @@ export const AddTemplateSettingsFormPartial = ({
<FormControl>
<DocumentGlobalAuthAccessSelect
{...field}
onValueChange={(value) => {
field.onChange(value);
void handleAutoSave();
}}
value={field.value}
disabled={field.disabled}
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
@ -275,7 +315,10 @@ export const AddTemplateSettingsFormPartial = ({
canUpdateVisibility={canUpdateVisibility}
currentTeamMemberRole={currentTeamMemberRole}
{...field}
onValueChange={field.onChange}
onValueChange={(value) => {
field.onChange(value);
void handleAutoSave();
}}
/>
</FormControl>
</FormItem>
@ -334,7 +377,13 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<Select
{...field}
onValueChange={(value) => {
field.onChange(value);
void handleAutoSave();
}}
>
<SelectTrigger className="bg-background text-muted-foreground">
<SelectValue data-testid="documentDistributionMethodSelectValue" />
</SelectTrigger>
@ -371,7 +420,10 @@ export const AddTemplateSettingsFormPartial = ({
value: option.value,
}))}
selectedValues={field.value}
onChange={field.onChange}
onChange={(value) => {
field.onChange(value);
void handleAutoSave();
}}
className="bg-background w-full"
emptySelectionPlaceholder="Select signature types"
/>
@ -395,9 +447,13 @@ export const AddTemplateSettingsFormPartial = ({
<FormControl>
<DocumentGlobalAuthActionSelect
{...field}
onValueChange={(value) => {
field.onChange(value);
void handleAutoSave();
}}
value={field.value}
disabled={field.disabled}
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
@ -468,7 +524,7 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Input {...field} />
<Input {...field} maxLength={254} />
</FormControl>
<FormMessage />
@ -488,7 +544,7 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Input {...field} />
<Input {...field} maxLength={254} onBlur={handleAutoSave} />
</FormControl>
<FormMessage />
@ -515,7 +571,12 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Textarea className="bg-background h-16 resize-none" {...field} />
<Textarea
className="bg-background h-16 resize-none"
{...field}
maxLength={5000}
onBlur={handleAutoSave}
/>
</FormControl>
<FormMessage />
@ -525,7 +586,12 @@ export const AddTemplateSettingsFormPartial = ({
<DocumentEmailCheckboxes
value={emailSettings}
onChange={(value) => form.setValue('meta.emailSettings', value)}
onChange={(value) => {
form.setValue('meta.emailSettings', value, {
shouldDirty: true,
});
void handleAutoSave();
}}
/>
</div>
</AccordionContent>
@ -563,7 +629,12 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
<Input
className="bg-background"
{...field}
maxLength={255}
onBlur={handleAutoSave}
/>
</FormControl>
<FormMessage />
@ -581,7 +652,13 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<Select
{...field}
onValueChange={(value) => {
field.onChange(value);
void handleAutoSave();
}}
>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
@ -615,7 +692,10 @@ export const AddTemplateSettingsFormPartial = ({
className="bg-background time-zone-field"
options={TIME_ZONES}
{...field}
onChange={(value) => value && field.onChange(value)}
onChange={(value) => {
value && field.onChange(value);
void handleAutoSave();
}}
/>
</FormControl>
@ -645,7 +725,12 @@ export const AddTemplateSettingsFormPartial = ({
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
<Input
className="bg-background"
{...field}
maxLength={255}
onBlur={handleAutoSave}
/>
</FormControl>
<FormMessage />

View File

@ -49,7 +49,10 @@ export const ZAddTemplateSettingsFormSchema = z.object({
.optional()
.default('en'),
emailId: z.string().nullable(),
emailReplyTo: z.string().optional(),
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,