feat: polish envelopes (#2090)

## Description

The rest of the owl
This commit is contained in:
David Nguyen
2025-10-24 16:22:06 +11:00
committed by GitHub
parent 88836404d1
commit 03eb6af69a
141 changed files with 5171 additions and 2402 deletions

View File

@ -1,31 +0,0 @@
// export const numberFormatValues = [
// {
// label: '123,456,789.00',
// value: '123,456,789.00',
// },
// {
// label: '123.456.789,00',
// value: '123.456.789,00',
// },
// {
// label: '123456,789.00',
// value: '123456,789.00',
// },
// ];
export const checkboxValidationRules = ['Select at least', 'Select exactly', 'Select at most'];
export const checkboxValidationLength = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
export const checkboxValidationSigns = [
{
label: 'Select at least',
value: '>=',
},
{
label: 'Select exactly',
value: '=',
},
{
label: 'Select at most',
value: '<=',
},
];

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from '@lingui/core/macro';
@ -7,11 +7,19 @@ import { PlusIcon, Trash } from 'lucide-react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { validateCheckboxLength } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
import {
type TCheckboxFieldMeta as CheckboxFieldMeta,
DEFAULT_FIELD_FONT_SIZE,
ZCheckboxFieldMeta,
} from '@documenso/lib/types/field-meta';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import {
checkboxValidationLength,
checkboxValidationRules,
checkboxValidationSigns,
} from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import {
Form,
FormControl,
@ -30,8 +38,8 @@ import {
} from '@documenso/ui/primitives/select';
import { Separator } from '@documenso/ui/primitives/separator';
import { checkboxValidationLength, checkboxValidationRules } from './constants';
import {
EditorGenericFontSizeField,
EditorGenericReadOnlyField,
EditorGenericRequiredField,
} from './editor-field-generic-field-forms';
@ -44,6 +52,7 @@ const ZCheckboxFieldFormSchema = ZCheckboxFieldMeta.pick({
required: true,
values: true,
readOnly: true,
fontSize: true,
})
.extend({
validationLength: z.coerce.number().optional(),
@ -90,6 +99,7 @@ export const EditorFieldCheckboxForm = ({
values: value.values || [{ id: 1, checked: false, value: '' }],
required: value.required || false,
readOnly: value.readOnly || false,
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
},
});
@ -99,13 +109,17 @@ export const EditorFieldCheckboxForm = ({
control,
});
const addValue = () => {
const addValue = (numberOfValues: number = 1) => {
const currentValues = form.getValues('values') || [];
const newId =
currentValues.length > 0 ? Math.max(...currentValues.map((val) => val.id)) + 1 : 1;
const currentMaxId = Math.max(...currentValues.map((val) => val.id));
const newValues = [...currentValues, { id: newId, checked: false, value: '' }];
form.setValue('values', newValues);
const newValues = Array.from({ length: numberOfValues }, (_, index) => ({
id: currentMaxId + index + 1,
checked: false,
value: '',
}));
form.setValue('values', [...currentValues, ...newValues]);
};
const removeValue = (index: number) => {
@ -132,10 +146,34 @@ export const EditorFieldCheckboxForm = ({
}
}, [formValues]);
const isValidationRuleMetForPreselectedValues = useMemo(() => {
const preselectedValues = (formValues.values || [])?.filter((value) => value.checked);
if (formValues.validationLength && formValues.validationRule && preselectedValues.length > 0) {
const validationRule = checkboxValidationSigns.find(
(sign) => sign.label === formValues.validationRule,
);
if (!validationRule) {
return false;
}
return validateCheckboxLength(
preselectedValues.length,
validationRule.value,
formValues.validationLength,
);
}
return true;
}, [formValues]);
return (
<Form {...form}>
<form>
<fieldset className="flex flex-col gap-2">
<EditorGenericFontSizeField formControl={form.control} />
<FormField
control={form.control}
name="direction"
@ -202,7 +240,25 @@ export const EditorFieldCheckboxForm = ({
<FormControl>
<Select
value={field.value ? String(field.value) : ''}
onValueChange={field.onChange}
onValueChange={(value) => {
const validationNumber = Number(value);
const currentValues = formValues.values || [];
const minimumNumberOfValuesRequired =
validationNumber - currentValues.length;
if (!formValues.validationRule) {
form.setValue('validationRule', checkboxValidationRules[0]);
}
if (minimumNumberOfValuesRequired > 0) {
addValue(minimumNumberOfValuesRequired);
}
field.onChange(validationNumber);
void form.trigger();
}}
>
<SelectTrigger className="text-muted-foreground bg-background mt-5 w-full">
<SelectValue placeholder={t`Pick a number`} />
@ -239,7 +295,7 @@ export const EditorFieldCheckboxForm = ({
<Trans>Checkbox values</Trans>
</p>
<button type="button" onClick={addValue}>
<button type="button" onClick={() => addValue()}>
<PlusIcon className="h-4 w-4" />
</button>
</div>
@ -285,6 +341,16 @@ export const EditorFieldCheckboxForm = ({
</li>
))}
</ul>
{!isValidationRuleMetForPreselectedValues && (
<Alert variant="warning">
<AlertDescription>
<Trans>
The preselected values will be ignored unless they meet the validation criteria.
</Trans>
</AlertDescription>
</Alert>
)}
</section>
</fieldset>
</form>

View File

@ -8,7 +8,10 @@ import { PlusIcon, Trash } from 'lucide-react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { type TDropdownFieldMeta as DropdownFieldMeta } from '@documenso/lib/types/field-meta';
import {
DEFAULT_FIELD_FONT_SIZE,
type TDropdownFieldMeta as DropdownFieldMeta,
} from '@documenso/lib/types/field-meta';
import {
Form,
FormControl,
@ -28,56 +31,50 @@ import {
import { Separator } from '@documenso/ui/primitives/separator';
import {
EditorGenericFontSizeField,
EditorGenericReadOnlyField,
EditorGenericRequiredField,
} from './editor-field-generic-field-forms';
const ZDropdownFieldFormSchema = z
.object({
defaultValue: z.string().optional(),
values: z
.object({
value: z.string().min(1, {
message: msg`Option value cannot be empty`.id,
}),
})
.array()
.min(1, {
message: msg`Dropdown must have at least one option`.id,
})
.refine(
(data) => {
// Todo: Envelopes - This doesn't work.
console.log({
data,
});
const ZDropdownFieldFormSchema = z.object({
defaultValue: z.string().optional(),
values: z
.object({
value: z.string().min(1, {
message: msg`Option value cannot be empty`.id,
}),
})
.array()
.min(1, {
message: msg`Dropdown must have at least one option`.id,
})
.superRefine((values, ctx) => {
const seen = new Map<string, number[]>(); // value → indices
if (data) {
const values = data.map((item) => item.value);
return new Set(values).size === values.length;
values.forEach((item, index) => {
const key = item.value;
if (!seen.has(key)) {
seen.set(key, []);
}
seen.get(key)!.push(index);
});
for (const [key, indices] of seen) {
if (indices.length > 1 && key.trim() !== '') {
for (const i of indices) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: msg`Duplicate values are not allowed`.id,
path: [i, 'value'],
});
}
return true;
},
{
message: 'Duplicate values are not allowed',
},
),
required: z.boolean().optional(),
readOnly: z.boolean().optional(),
})
.refine(
(data) => {
// Default value must be one of the available options
if (data.defaultValue && data.values) {
return data.values.some((item) => item.value === data.defaultValue);
}
}
return true;
},
{
message: 'Default value must be one of the available options',
path: ['defaultValue'],
},
);
}),
required: z.boolean().optional(),
readOnly: z.boolean().optional(),
fontSize: z.number().optional(),
});
type TDropdownFieldFormSchema = z.infer<typeof ZDropdownFieldFormSchema>;
@ -102,6 +99,7 @@ export const EditorFieldDropdownForm = ({
values: value.values || [{ value: 'Option 1' }],
required: value.required || false,
readOnly: value.readOnly || false,
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
},
});
@ -111,7 +109,20 @@ export const EditorFieldDropdownForm = ({
const addValue = () => {
const currentValues = form.getValues('values') || [];
const newValues = [...currentValues, { value: 'New option' }];
let newValue = 'New option';
// Iterate to create a unique value
for (let i = 0; i < currentValues.length; i++) {
newValue = `New option ${i + 1}`;
if (currentValues.some((item) => item.value === `New option ${i + 1}`)) {
newValue = `New option ${i + 1}`;
} else {
break;
}
}
const newValues = [...currentValues, { value: newValue }];
form.setValue('values', newValues);
};
@ -127,6 +138,10 @@ export const EditorFieldDropdownForm = ({
newValues.splice(index, 1);
form.setValue('values', newValues);
if (form.getValues('defaultValue') === newValues[index].value) {
form.setValue('defaultValue', undefined);
}
};
useEffect(() => {
@ -140,19 +155,13 @@ export const EditorFieldDropdownForm = ({
}
}, [formValues]);
const { formState } = form;
useEffect(() => {
console.log({
errors: formState.errors,
formValues,
});
}, [formState, formState.errors, formValues]);
return (
<Form {...form}>
<form>
<fieldset className="flex flex-col gap-2">
<EditorGenericFontSizeField formControl={form.control} />
{/* Todo: Envelopes This is buggy. */}
<FormField
control={form.control}
name="defaultValue"
@ -163,20 +172,25 @@ export const EditorFieldDropdownForm = ({
</FormLabel>
<FormControl>
<Select
// Todo: Envelopes - This is buggy, removing/adding should update the default value.
{...field}
value={field.value}
onValueChange={(val) => field.onChange(val)}
value={field.value ?? '-1'}
onValueChange={(value) => field.onChange(value === '-1' ? undefined : value)}
>
<SelectTrigger className="text-muted-foreground bg-background w-full">
<SelectValue placeholder={t`Default Value`} />
</SelectTrigger>
<SelectContent position="popper">
{(formValues.values || []).map((item, index) => (
<SelectItem key={index} value={item.value || ''}>
{item.value}
</SelectItem>
))}
{(formValues.values || [])
.filter((item) => item.value)
.map((item, index) => (
<SelectItem key={index} value={item.value || ''}>
{item.value}
</SelectItem>
))}
<SelectItem value={'-1'}>
<Trans>Default Value</Trans>
</SelectItem>
</SelectContent>
</Select>
</FormControl>

View File

@ -130,6 +130,12 @@ export const EditorFieldNumberForm = ({
<Form {...form}>
<form>
<fieldset className="flex flex-col gap-2">
<div className="flex w-full flex-row gap-x-4">
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
</div>
<EditorGenericLabelField formControl={form.control} />
<FormField
@ -198,12 +204,6 @@ export const EditorFieldNumberForm = ({
)}
/>
<div className="flex w-full flex-row gap-x-4">
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
</div>
<div className="mt-1">
<EditorGenericRequiredField formControl={form.control} />
</div>

View File

@ -1,47 +1,62 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { PlusIcon, Trash } from 'lucide-react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import type { z } from 'zod';
import { type TRadioFieldMeta as RadioFieldMeta } from '@documenso/lib/types/field-meta';
import {
DEFAULT_FIELD_FONT_SIZE,
type TRadioFieldMeta as RadioFieldMeta,
ZRadioFieldMeta,
} from '@documenso/lib/types/field-meta';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import { Form, FormControl, FormField, FormItem } from '@documenso/ui/primitives/form/form';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { Separator } from '@documenso/ui/primitives/separator';
import {
EditorGenericFontSizeField,
EditorGenericReadOnlyField,
EditorGenericRequiredField,
} from './editor-field-generic-field-forms';
const ZRadioFieldFormSchema = z
.object({
label: z.string().optional(),
values: z
.object({ id: z.number(), checked: z.boolean(), value: z.string() })
.array()
.min(1)
.optional(),
required: z.boolean().optional(),
readOnly: z.boolean().optional(),
})
.refine(
(data) => {
// There cannot be more than one checked option
if (data.values) {
const checkedValues = data.values.filter((option) => option.checked);
return checkedValues.length <= 1;
}
return true;
},
{
message: 'There cannot be more than one checked option',
path: ['values'],
},
);
const ZRadioFieldFormSchema = ZRadioFieldMeta.pick({
label: true,
direction: true,
values: true,
required: true,
readOnly: true,
fontSize: true,
}).refine(
(data) => {
// There cannot be more than one checked option
if (data.values) {
const checkedValues = data.values.filter((option) => option.checked);
return checkedValues.length <= 1;
}
return true;
},
{
message: 'There cannot be more than one checked option',
path: ['values'],
},
);
type TRadioFieldFormSchema = z.infer<typeof ZRadioFieldFormSchema>;
@ -53,9 +68,12 @@ export type EditorFieldRadioFormProps = {
export const EditorFieldRadioForm = ({
value = {
type: 'radio',
direction: 'vertical',
},
onValueChange,
}: EditorFieldRadioFormProps) => {
const { t } = useLingui();
const form = useForm<TRadioFieldFormSchema>({
resolver: zodResolver(ZRadioFieldFormSchema),
mode: 'onChange',
@ -64,6 +82,8 @@ export const EditorFieldRadioForm = ({
values: value.values || [{ id: 1, checked: false, value: 'Default value' }],
required: value.required || false,
readOnly: value.readOnly || false,
direction: value.direction || 'vertical',
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
},
});
@ -107,7 +127,37 @@ export const EditorFieldRadioForm = ({
return (
<Form {...form}>
<form>
<fieldset className="flex flex-col gap-2 pb-2">
<fieldset className="flex flex-col gap-2">
<EditorGenericFontSizeField formControl={form.control} />
<FormField
control={form.control}
name="direction"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Direction</Trans>
</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="text-muted-foreground bg-background w-full">
<SelectValue placeholder={t`Select direction`} />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="vertical">
<Trans>Vertical</Trans>
</SelectItem>
<SelectItem value="horizontal">
<Trans>Horizontal</Trans>
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<EditorGenericRequiredField formControl={form.control} />
<EditorGenericReadOnlyField formControl={form.control} />

View File

@ -0,0 +1,68 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod';
import {
DEFAULT_FIELD_FONT_SIZE,
type TSignatureFieldMeta,
ZSignatureFieldMeta,
} from '@documenso/lib/types/field-meta';
import { Form } from '@documenso/ui/primitives/form/form';
import { EditorGenericFontSizeField } from './editor-field-generic-field-forms';
const ZSignatureFieldFormSchema = ZSignatureFieldMeta.pick({
fontSize: true,
});
type TSignatureFieldFormSchema = z.infer<typeof ZSignatureFieldFormSchema>;
type EditorFieldSignatureFormProps = {
value: TSignatureFieldMeta | undefined;
onValueChange: (value: TSignatureFieldMeta) => void;
};
export const EditorFieldSignatureForm = ({
value = {
type: 'signature',
},
onValueChange,
}: EditorFieldSignatureFormProps) => {
const form = useForm<TSignatureFieldFormSchema>({
resolver: zodResolver(ZSignatureFieldFormSchema),
mode: 'onChange',
defaultValues: {
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
},
});
const { control } = form;
const formValues = useWatch({
control,
});
// Dupecode/Inefficient: Done because native isValid won't work for our usecase.
useEffect(() => {
const validatedFormValues = ZSignatureFieldFormSchema.safeParse(formValues);
if (validatedFormValues.success) {
onValueChange({
type: 'signature',
...validatedFormValues.data,
});
}
}, [formValues]);
return (
<Form {...form}>
<form>
<fieldset className="flex flex-col gap-2">
<EditorGenericFontSizeField formControl={form.control} />
</fieldset>
</form>
</Form>
);
};

View File

@ -5,7 +5,10 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { type TTextFieldMeta as TextFieldMeta } from '@documenso/lib/types/field-meta';
import {
DEFAULT_FIELD_FONT_SIZE,
type TTextFieldMeta as TextFieldMeta,
} from '@documenso/lib/types/field-meta';
import {
Form,
FormControl,
@ -69,7 +72,7 @@ export const EditorFieldTextForm = ({
placeholder: value.placeholder || '',
text: value.text || '',
characterLimit: value.characterLimit || 0,
fontSize: value.fontSize || 14,
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
textAlign: value.textAlign || 'left',
required: value.required || false,
readOnly: value.readOnly || false,
@ -98,6 +101,12 @@ export const EditorFieldTextForm = ({
<Form {...form}>
<form>
<fieldset className="flex flex-col gap-2">
<div className="flex w-full flex-row gap-x-4">
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
</div>
<FormField
control={form.control}
name="label"
@ -173,12 +182,6 @@ export const EditorFieldTextForm = ({
)}
/>
<div className="flex w-full flex-row gap-x-4">
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
</div>
<div className="mt-1">
<EditorGenericRequiredField formControl={form.control} />
</div>