Compare commits
17 Commits
v2.0.6
...
7fa86fe297
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fa86fe297 | |||
| 0b91d33bfb | |||
| e010238bcc | |||
| 498a2be1c7 | |||
| 3e84aa632f | |||
| a08a77e98b | |||
| 13d9ca7a0e | |||
| d25565b7d0 | |||
| 91421a7d62 | |||
| a9f1e39b10 | |||
| b37748654e | |||
| b3ed80d721 | |||
| b3cb750470 | |||
| 1e52493144 | |||
| ab95e80987 | |||
| 1780a5c262 | |||
| cb9bf407f7 |
@ -336,7 +336,7 @@ export const EnvelopeDistributeDialog = ({
|
||||
<Trans>Message</Trans>{' '}
|
||||
<span className="text-muted-foreground">(Optional)</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground p-4">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
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 { createCallable } from 'react-call';
|
||||
@ -27,71 +28,49 @@ import {
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => {
|
||||
let schema = z.coerce.number({
|
||||
invalid_type_error: msg`Please enter a valid number`.id,
|
||||
});
|
||||
|
||||
const { numberFormat, minValue, maxValue } = fieldMeta;
|
||||
|
||||
if (typeof minValue === 'number') {
|
||||
schema = schema.min(minValue);
|
||||
}
|
||||
|
||||
if (typeof maxValue === 'number') {
|
||||
schema = schema.max(maxValue);
|
||||
}
|
||||
|
||||
if (numberFormat) {
|
||||
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
|
||||
|
||||
if (!foundRegex) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
return schema.refine(
|
||||
(value) => {
|
||||
return foundRegex.test(value.toString());
|
||||
},
|
||||
{
|
||||
message: msg`Number needs to be formatted as ${numberFormat}`.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
export type SignFieldNumberDialogProps = {
|
||||
fieldMeta: TNumberFieldMeta;
|
||||
};
|
||||
|
||||
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, string | null>(
|
||||
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, number | null>(
|
||||
({ call, fieldMeta }) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
// Needs to be inside dialog for translation purposes.
|
||||
const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => {
|
||||
const { numberFormat, minValue, maxValue } = fieldMeta;
|
||||
|
||||
if (numberFormat) {
|
||||
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
|
||||
|
||||
if (foundRegex) {
|
||||
return z.string().refine(
|
||||
(value) => {
|
||||
return foundRegex.test(value.toString());
|
||||
},
|
||||
{
|
||||
message: t`Number needs to be formatted as ${numberFormat}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Not gong to work with min/max numbers + number format
|
||||
// Since currently doesn't work in V1 going to ignore for now.
|
||||
return z.string().superRefine((value, ctx) => {
|
||||
const isValidNumber = /^[0-9,.]+$/.test(value.toString());
|
||||
|
||||
if (!isValidNumber) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t`Please enter a valid number`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof minValue === 'number' && parseFloat(value) < minValue) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.too_small,
|
||||
minimum: minValue,
|
||||
inclusive: true,
|
||||
type: 'number',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof maxValue === 'number' && parseFloat(value) > maxValue) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.too_big,
|
||||
maximum: maxValue,
|
||||
inclusive: true,
|
||||
type: 'number',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const ZSignFieldNumberFormSchema = z.object({
|
||||
number: createNumberFieldSchema(fieldMeta),
|
||||
});
|
||||
|
||||
@ -336,7 +336,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
<div className="flex-1">
|
||||
<PDFViewer
|
||||
envelopeItem={envelopeItems[0]}
|
||||
token={recipient.token}
|
||||
token={token}
|
||||
version="signed"
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
|
||||
@ -7,7 +7,6 @@ import type { z } from 'zod';
|
||||
import {
|
||||
DEFAULT_FIELD_FONT_SIZE,
|
||||
type TDateFieldMeta as DateFieldMeta,
|
||||
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
ZDateFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { Form } from '@documenso/ui/primitives/form/form';
|
||||
@ -40,7 +39,7 @@ export const EditorFieldDateForm = ({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
textAlign: value.textAlign || 'left',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ import type { z } from 'zod';
|
||||
import {
|
||||
DEFAULT_FIELD_FONT_SIZE,
|
||||
type TEmailFieldMeta as EmailFieldMeta,
|
||||
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
ZEmailFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { Form } from '@documenso/ui/primitives/form/form';
|
||||
@ -40,7 +39,7 @@ export const EditorFieldEmailForm = ({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
textAlign: value.textAlign || 'left',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -3,10 +3,6 @@ import { useEffect } from 'react';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { type Control, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { FIELD_MIN_LINE_HEIGHT } from '@documenso/lib/types/field-meta';
|
||||
import { FIELD_MAX_LINE_HEIGHT } from '@documenso/lib/types/field-meta';
|
||||
import { FIELD_MIN_LETTER_SPACING } from '@documenso/lib/types/field-meta';
|
||||
import { FIELD_MAX_LETTER_SPACING } from '@documenso/lib/types/field-meta';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||
import {
|
||||
@ -111,119 +107,6 @@ export const EditorGenericTextAlignField = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorGenericVerticalAlignField = ({
|
||||
formControl,
|
||||
className,
|
||||
}: {
|
||||
formControl: FormControlType;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={formControl}
|
||||
name="verticalAlign"
|
||||
render={({ field }) => (
|
||||
<FormItem className={className}>
|
||||
<FormLabel>
|
||||
<Trans>Vertical Align</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t`Select vertical align`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="top">
|
||||
<Trans>Top</Trans>
|
||||
</SelectItem>
|
||||
<SelectItem value="middle">
|
||||
<Trans>Middle</Trans>
|
||||
</SelectItem>
|
||||
<SelectItem value="bottom">
|
||||
<Trans>Bottom</Trans>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorGenericLineHeightField = ({
|
||||
formControl,
|
||||
className,
|
||||
}: {
|
||||
formControl: FormControlType;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={formControl}
|
||||
name="lineHeight"
|
||||
render={({ field }) => (
|
||||
<FormItem className={className}>
|
||||
<FormLabel>
|
||||
<Trans>Line Height</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={FIELD_MIN_LINE_HEIGHT}
|
||||
max={FIELD_MAX_LINE_HEIGHT}
|
||||
className="bg-background"
|
||||
placeholder={t`Line height`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorGenericLetterSpacingField = ({
|
||||
formControl,
|
||||
className,
|
||||
}: {
|
||||
formControl: FormControlType;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={formControl}
|
||||
name="letterSpacing"
|
||||
render={({ field }) => (
|
||||
<FormItem className={className}>
|
||||
<FormLabel>
|
||||
<Trans>Letter Spacing</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={FIELD_MIN_LETTER_SPACING}
|
||||
max={FIELD_MAX_LETTER_SPACING}
|
||||
className="bg-background"
|
||||
placeholder={t`Letter spacing`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorGenericRequiredField = ({
|
||||
formControl,
|
||||
className,
|
||||
|
||||
@ -6,7 +6,6 @@ import type { z } from 'zod';
|
||||
|
||||
import {
|
||||
DEFAULT_FIELD_FONT_SIZE,
|
||||
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
type TInitialsFieldMeta as InitialsFieldMeta,
|
||||
ZInitialsFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
@ -40,7 +39,7 @@ export const EditorFieldInitialsForm = ({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
textAlign: value.textAlign || 'left',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import type { z } from 'zod';
|
||||
|
||||
import {
|
||||
DEFAULT_FIELD_FONT_SIZE,
|
||||
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
type TNameFieldMeta as NameFieldMeta,
|
||||
ZNameFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
@ -40,7 +39,7 @@ export const EditorFieldNameForm = ({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
textAlign: value.textAlign || 'left',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -6,11 +6,6 @@ import { useForm, useWatch } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import {
|
||||
DEFAULT_FIELD_FONT_SIZE,
|
||||
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||
FIELD_DEFAULT_LETTER_SPACING,
|
||||
FIELD_DEFAULT_LINE_HEIGHT,
|
||||
type TNumberFieldMeta as NumberFieldMeta,
|
||||
ZNumberFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
@ -36,12 +31,9 @@ import { Separator } from '@documenso/ui/primitives/separator';
|
||||
import {
|
||||
EditorGenericFontSizeField,
|
||||
EditorGenericLabelField,
|
||||
EditorGenericLetterSpacingField,
|
||||
EditorGenericLineHeightField,
|
||||
EditorGenericReadOnlyField,
|
||||
EditorGenericRequiredField,
|
||||
EditorGenericTextAlignField,
|
||||
EditorGenericVerticalAlignField,
|
||||
} from './editor-field-generic-field-forms';
|
||||
|
||||
const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
||||
@ -51,9 +43,6 @@ const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
||||
numberFormat: true,
|
||||
fontSize: true,
|
||||
textAlign: true,
|
||||
lineHeight: true,
|
||||
letterSpacing: true,
|
||||
verticalAlign: true,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
minValue: true,
|
||||
@ -110,11 +99,8 @@ export const EditorFieldNumberForm = ({
|
||||
placeholder: value.placeholder || '',
|
||||
value: value.value || '',
|
||||
numberFormat: value.numberFormat || null,
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
lineHeight: value.lineHeight ?? FIELD_DEFAULT_LINE_HEIGHT,
|
||||
letterSpacing: value.letterSpacing ?? FIELD_DEFAULT_LETTER_SPACING,
|
||||
verticalAlign: value.verticalAlign ?? FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||
fontSize: value.fontSize || 14,
|
||||
textAlign: value.textAlign || 'left',
|
||||
required: value.required || false,
|
||||
readOnly: value.readOnly || false,
|
||||
minValue: value.minValue,
|
||||
@ -132,10 +118,6 @@ export const EditorFieldNumberForm = ({
|
||||
useEffect(() => {
|
||||
const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues);
|
||||
|
||||
if (formValues.readOnly && !formValues.value) {
|
||||
void form.trigger('value');
|
||||
}
|
||||
|
||||
if (validatedFormValues.success) {
|
||||
onValueChange({
|
||||
type: 'number',
|
||||
@ -148,12 +130,10 @@ export const EditorFieldNumberForm = ({
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<fieldset className="flex flex-col gap-2">
|
||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||
|
||||
<div className="flex w-full flex-row gap-x-4">
|
||||
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||
|
||||
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} />
|
||||
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||
</div>
|
||||
|
||||
<EditorGenericLabelField formControl={form.control} />
|
||||
@ -224,12 +204,6 @@ export const EditorFieldNumberForm = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex w-full flex-row gap-x-4">
|
||||
<EditorGenericLineHeightField className="w-full" formControl={form.control} />
|
||||
|
||||
<EditorGenericLetterSpacingField className="w-full" formControl={form.control} />
|
||||
</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<EditorGenericRequiredField formControl={form.control} />
|
||||
</div>
|
||||
|
||||
@ -5,8 +5,11 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '@documenso/lib/constants/pdf';
|
||||
import { type TSignatureFieldMeta, ZSignatureFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
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';
|
||||
@ -32,7 +35,7 @@ export const EditorFieldSignatureForm = ({
|
||||
resolver: zodResolver(ZSignatureFieldFormSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_SIGNATURE_TEXT_FONT_SIZE,
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -3,16 +3,11 @@ import { useEffect } from 'react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
DEFAULT_FIELD_FONT_SIZE,
|
||||
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||
FIELD_DEFAULT_LETTER_SPACING,
|
||||
FIELD_DEFAULT_LINE_HEIGHT,
|
||||
type TTextFieldMeta as TextFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
Form,
|
||||
@ -27,36 +22,32 @@ import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
|
||||
import {
|
||||
EditorGenericFontSizeField,
|
||||
EditorGenericLetterSpacingField,
|
||||
EditorGenericLineHeightField,
|
||||
EditorGenericReadOnlyField,
|
||||
EditorGenericRequiredField,
|
||||
EditorGenericTextAlignField,
|
||||
EditorGenericVerticalAlignField,
|
||||
} from './editor-field-generic-field-forms';
|
||||
|
||||
const ZTextFieldFormSchema = ZTextFieldMeta.pick({
|
||||
label: true,
|
||||
placeholder: true,
|
||||
text: true,
|
||||
characterLimit: true,
|
||||
fontSize: true,
|
||||
textAlign: true,
|
||||
lineHeight: true,
|
||||
letterSpacing: true,
|
||||
verticalAlign: true,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
}).refine(
|
||||
(data) => {
|
||||
// A read-only field must have text
|
||||
return !data.readOnly || (data.text && data.text.length > 0);
|
||||
},
|
||||
{
|
||||
message: 'A read-only field must have text',
|
||||
path: ['text'],
|
||||
},
|
||||
);
|
||||
const ZTextFieldFormSchema = z
|
||||
.object({
|
||||
label: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
characterLimit: z.coerce.number().min(0).optional(),
|
||||
fontSize: z.coerce.number().min(8).max(96).optional(),
|
||||
textAlign: z.enum(['left', 'center', 'right']).optional(),
|
||||
required: z.boolean().optional(),
|
||||
readOnly: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// A read-only field must have text
|
||||
return !data.readOnly || (data.text && data.text.length > 0);
|
||||
},
|
||||
{
|
||||
message: 'A read-only field must have text',
|
||||
path: ['text'],
|
||||
},
|
||||
);
|
||||
|
||||
type TTextFieldFormSchema = z.infer<typeof ZTextFieldFormSchema>;
|
||||
|
||||
@ -82,10 +73,7 @@ export const EditorFieldTextForm = ({
|
||||
text: value.text || '',
|
||||
characterLimit: value.characterLimit || 0,
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
lineHeight: value.lineHeight ?? FIELD_DEFAULT_LINE_HEIGHT,
|
||||
letterSpacing: value.letterSpacing ?? FIELD_DEFAULT_LETTER_SPACING,
|
||||
verticalAlign: value.verticalAlign ?? FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||
textAlign: value.textAlign || 'left',
|
||||
required: value.required || false,
|
||||
readOnly: value.readOnly || false,
|
||||
},
|
||||
@ -101,10 +89,6 @@ export const EditorFieldTextForm = ({
|
||||
useEffect(() => {
|
||||
const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues);
|
||||
|
||||
if (formValues.readOnly && !formValues.text) {
|
||||
void form.trigger('text');
|
||||
}
|
||||
|
||||
if (validatedFormValues.success) {
|
||||
onValueChange({
|
||||
type: 'text',
|
||||
@ -117,12 +101,10 @@ export const EditorFieldTextForm = ({
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<fieldset className="flex flex-col gap-2">
|
||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||
|
||||
<div className="flex w-full flex-row gap-x-4">
|
||||
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||
|
||||
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} />
|
||||
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
@ -200,16 +182,17 @@ export const EditorFieldTextForm = ({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
className="bg-background"
|
||||
placeholder={t`Character limit`}
|
||||
placeholder={t`Field character limit`}
|
||||
{...field}
|
||||
value={field.value || ''}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
|
||||
const values = form.getValues();
|
||||
const characterLimit = parseInt(e.target.value, 10) || 0;
|
||||
|
||||
field.onChange(characterLimit || '');
|
||||
|
||||
const textValue = values.text || '';
|
||||
|
||||
if (characterLimit > 0 && textValue.length > characterLimit) {
|
||||
@ -223,12 +206,6 @@ export const EditorFieldTextForm = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex w-full flex-row gap-x-4">
|
||||
<EditorGenericLineHeightField className="w-full" formControl={form.control} />
|
||||
|
||||
<EditorGenericLetterSpacingField className="w-full" formControl={form.control} />
|
||||
</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<EditorGenericRequiredField formControl={form.control} />
|
||||
</div>
|
||||
|
||||
@ -184,10 +184,10 @@ export const DocumentSigningPageViewV2 = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="embed--DocumentWidgetFooter mt-auto">
|
||||
<div className="embed--DocumentWidgetFooter">
|
||||
{/* Footer of left sidebar. */}
|
||||
{!isEmbed && (
|
||||
<div className="px-4">
|
||||
<div className="mt-auto px-4">
|
||||
<Button asChild variant="ghost" className="w-full justify-start">
|
||||
<Link to="/">
|
||||
<ArrowLeftIcon className="mr-2 h-4 w-4" />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client';
|
||||
import { type DocumentData, DocumentStatus, type EnvelopeItem } from '@prisma/client';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
@ -100,14 +100,7 @@ export const DocumentCertificateQRView = ({
|
||||
)}
|
||||
|
||||
{internalVersion === 2 ? (
|
||||
<EnvelopeRenderProvider
|
||||
envelope={{
|
||||
envelopeItems,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
}}
|
||||
token={token}
|
||||
>
|
||||
<EnvelopeRenderProvider envelope={{ envelopeItems }} token={token}>
|
||||
<DocumentCertificateQrV2
|
||||
title={title}
|
||||
recipientCount={recipientCount}
|
||||
@ -137,7 +130,7 @@ export const DocumentCertificateQRView = ({
|
||||
envelopeItems={envelopeItems}
|
||||
token={token}
|
||||
trigger={
|
||||
<Button type="button" variant="outline" className="w-fit">
|
||||
<Button type="button" variant="outline" className="flex-1">
|
||||
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
@ -196,7 +189,7 @@ const DocumentCertificateQrV2 = ({
|
||||
envelopeItems={envelopeItems}
|
||||
token={token}
|
||||
trigger={
|
||||
<Button type="button" variant="outline" className="w-fit">
|
||||
<Button type="button" variant="outline" className="flex-1">
|
||||
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
|
||||
@ -7,7 +7,6 @@ import { DateTime } from 'luxon';
|
||||
|
||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
|
||||
export type DocumentPageViewInformationProps = {
|
||||
userId: number;
|
||||
@ -41,10 +40,6 @@ export const DocumentPageViewInformation = ({
|
||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||
.toRelative(),
|
||||
},
|
||||
{
|
||||
description: msg`Document ID (Legacy)`,
|
||||
value: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||
},
|
||||
];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isMounted, envelope, userId]);
|
||||
|
||||
@ -616,14 +616,13 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 50,
|
||||
}}
|
||||
// Don't use darkmode for this component, it should look the same for both light/dark modes.
|
||||
className="grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border border-gray-300 bg-white p-1 text-gray-500 shadow-sm"
|
||||
className="text-muted-foreground grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border bg-white p-1 shadow-sm"
|
||||
>
|
||||
{fieldButtonList.map((field) => (
|
||||
<button
|
||||
key={field.type}
|
||||
onClick={() => createFieldFromPendingTemplate(pendingFieldCreation, field.type)}
|
||||
className="col-span-1 w-full flex-shrink-0 rounded-sm px-2 py-1 text-xs hover:bg-gray-100 hover:text-gray-600"
|
||||
className="hover:text-foreground col-span-1 w-full flex-shrink-0 rounded-sm px-2 py-1 text-xs hover:bg-gray-100"
|
||||
>
|
||||
{t(field.name)}
|
||||
</button>
|
||||
|
||||
@ -2,7 +2,7 @@ import { lazy, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { faker } from '@faker-js/faker/locale/en';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { FieldType, SigningStatus } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { FileTextIcon } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -201,10 +201,7 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
envelope={envelope}
|
||||
token={undefined}
|
||||
fields={fieldsWithPlaceholders}
|
||||
recipients={envelope.recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
}))}
|
||||
recipients={envelope.recipients}
|
||||
overrideSettings={{
|
||||
mode: 'export',
|
||||
}}
|
||||
|
||||
@ -212,7 +212,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
);
|
||||
|
||||
const hasDocumentBeenSent = recipients.some(
|
||||
(recipient) => recipient.role !== RecipientRole.CC && recipient.sendStatus === SendStatus.SENT,
|
||||
(recipient) => recipient.sendStatus === SendStatus.SENT,
|
||||
);
|
||||
|
||||
const canRecipientBeModified = (recipientId?: number) => {
|
||||
|
||||
@ -49,7 +49,7 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const { t } = useLingui();
|
||||
const { envelope, setLocalEnvelope, relativePath, editorFields } = useCurrentEnvelopeEditor();
|
||||
const { envelope, setLocalEnvelope, relativePath } = useCurrentEnvelopeEditor();
|
||||
const { maximumEnvelopeItemCount, remaining } = useLimits();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -165,17 +165,9 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
const onFileDelete = (envelopeItemId: string) => {
|
||||
setLocalFiles((prev) => prev.filter((uploadingFile) => uploadingFile.id !== envelopeItemId));
|
||||
|
||||
const fieldsWithoutDeletedItem = envelope.fields.filter(
|
||||
(field) => field.envelopeItemId !== envelopeItemId,
|
||||
);
|
||||
|
||||
setLocalEnvelope({
|
||||
envelopeItems: envelope.envelopeItems.filter((item) => item.id !== envelopeItemId),
|
||||
fields: envelope.fields.filter((field) => field.envelopeItemId !== envelopeItemId),
|
||||
});
|
||||
|
||||
// Reset editor fields.
|
||||
editorFields.resetForm(fieldsWithoutDeletedItem);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { DocumentStatus, type Recipient, SigningStatus } from '@prisma/client';
|
||||
import { type Recipient, SigningStatus } from '@prisma/client';
|
||||
import type Konva from 'konva';
|
||||
|
||||
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||
@ -19,7 +19,6 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const {
|
||||
envelopeStatus,
|
||||
currentEnvelopeItem,
|
||||
fields,
|
||||
recipients,
|
||||
@ -43,10 +42,6 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
const { _className, scale } = pageContext;
|
||||
|
||||
const localPageFields = useMemo((): GenericLocalField[] => {
|
||||
if (envelopeStatus === DocumentStatus.COMPLETED) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fields
|
||||
.filter(
|
||||
(field) =>
|
||||
@ -59,20 +54,11 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
throw new Error(`Recipient not found for field ${field.id}`);
|
||||
}
|
||||
|
||||
const isInserted = recipient.signingStatus === SigningStatus.SIGNED && field.inserted;
|
||||
|
||||
return {
|
||||
...field,
|
||||
inserted: isInserted,
|
||||
customText: isInserted ? field.customText : '',
|
||||
recipient,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
({ inserted, fieldMeta, recipient }) =>
|
||||
(recipient.signingStatus === SigningStatus.SIGNED ? inserted : true) ||
|
||||
fieldMeta?.readOnly,
|
||||
);
|
||||
});
|
||||
}, [fields, pageContext.pageNumber, currentEnvelopeItem?.id, recipients]);
|
||||
|
||||
const unsafeRenderFieldOnLayer = (field: GenericLocalField) => {
|
||||
@ -81,8 +67,12 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { recipient } = field;
|
||||
|
||||
const fieldTranslations = getClientSideFieldTranslations(i18n);
|
||||
|
||||
const isInserted = recipient.signingStatus === SigningStatus.SIGNED && field.inserted;
|
||||
|
||||
renderField({
|
||||
scale,
|
||||
pageLayer: pageLayer.current,
|
||||
@ -93,6 +83,7 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
height: Number(field.height),
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
customText: isInserted ? field.customText : '',
|
||||
fieldMeta: field.fieldMeta,
|
||||
signature: {
|
||||
signatureImageAsBase64: '',
|
||||
@ -104,7 +95,7 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
pageHeight: unscaledViewport.height,
|
||||
color: getRecipientColorKey(field.recipientId),
|
||||
editable: false,
|
||||
mode: overrideSettings?.mode ?? 'edit',
|
||||
mode: overrideSettings?.mode ?? 'sign',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
type Field,
|
||||
FieldType,
|
||||
type Recipient,
|
||||
RecipientRole,
|
||||
type Signature,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
import { type Field, FieldType, RecipientRole, type Signature } from '@prisma/client';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { match } from 'ts-pattern';
|
||||
@ -19,7 +12,6 @@ import { useOptionalSession } from '@documenso/lib/client-only/providers/session
|
||||
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
|
||||
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { ZFullFieldSchema } from '@documenso/lib/types/field';
|
||||
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
|
||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||
@ -27,7 +19,6 @@ import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields
|
||||
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
|
||||
import { EnvelopeRecipientFieldTooltip } from '@documenso/ui/components/document/envelope-recipient-field-tooltip';
|
||||
import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
|
||||
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
@ -45,10 +36,6 @@ import { handleTextFieldClick } from '~/utils/field-signing/text-field';
|
||||
import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
|
||||
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
||||
|
||||
type GenericLocalField = TEnvelope['fields'][number] & {
|
||||
recipient: Pick<Recipient, 'id' | 'name' | 'email' | 'signingStatus'>;
|
||||
};
|
||||
|
||||
export default function EnvelopeSignerPageRenderer() {
|
||||
const { t, i18n } = useLingui();
|
||||
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
|
||||
@ -104,36 +91,6 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
);
|
||||
}, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]);
|
||||
|
||||
/**
|
||||
* Returns fields that have been fully signed by other recipients for this specific
|
||||
* page.
|
||||
*/
|
||||
const localPageOtherRecipientFields = useMemo((): GenericLocalField[] => {
|
||||
const signedRecipients = envelope.recipients.filter(
|
||||
(recipient) => recipient.signingStatus === SigningStatus.SIGNED,
|
||||
);
|
||||
|
||||
return signedRecipients.flatMap((recipient) => {
|
||||
return recipient.fields
|
||||
.filter(
|
||||
(field) =>
|
||||
field.page === pageContext.pageNumber &&
|
||||
field.envelopeItemId === currentEnvelopeItem?.id &&
|
||||
(field.inserted || field.fieldMeta?.readOnly),
|
||||
)
|
||||
.map((field) => ({
|
||||
...field,
|
||||
recipient: {
|
||||
id: recipient.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
signingStatus: recipient.signingStatus,
|
||||
role: recipient.role,
|
||||
},
|
||||
}));
|
||||
});
|
||||
}, [envelope.recipients, pageContext.pageNumber]);
|
||||
|
||||
const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
|
||||
if (!pageLayer.current) {
|
||||
console.error('Layer not loaded yet');
|
||||
@ -419,46 +376,6 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
}
|
||||
};
|
||||
|
||||
const renderFields = () => {
|
||||
if (!pageLayer.current) {
|
||||
console.error('Layer not loaded yet');
|
||||
return;
|
||||
}
|
||||
|
||||
// Render current recipient fields.
|
||||
for (const field of localPageFields) {
|
||||
renderFieldOnLayer(field);
|
||||
}
|
||||
|
||||
// Render other recipient signed and inserted fields.
|
||||
for (const field of localPageOtherRecipientFields) {
|
||||
try {
|
||||
renderField({
|
||||
scale,
|
||||
pageLayer: pageLayer.current,
|
||||
field: {
|
||||
renderId: field.id.toString(),
|
||||
...field,
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
fieldMeta: field.fieldMeta,
|
||||
},
|
||||
translations: getClientSideFieldTranslations(i18n),
|
||||
pageWidth: unscaledViewport.width,
|
||||
pageHeight: unscaledViewport.height,
|
||||
color: 'readOnly',
|
||||
editable: false,
|
||||
mode: 'sign',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Unable to render one or more fields belonging to other recipients.');
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const signField = async (
|
||||
fieldId: number,
|
||||
payload: TSignEnvelopeFieldValue,
|
||||
@ -495,7 +412,11 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
* Initialize the Konva page canvas and all fields and interactions.
|
||||
*/
|
||||
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||
renderFields();
|
||||
// Render the fields.
|
||||
for (const field of localPageFields) {
|
||||
renderFieldOnLayer(field);
|
||||
}
|
||||
|
||||
currentPageLayer.batchDraw();
|
||||
};
|
||||
|
||||
@ -507,7 +428,9 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
return;
|
||||
}
|
||||
|
||||
renderFields();
|
||||
localPageFields.forEach((field) => {
|
||||
renderFieldOnLayer(field);
|
||||
});
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]);
|
||||
@ -523,7 +446,9 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
// Rerender the whole page.
|
||||
pageLayer.current.destroyChildren();
|
||||
|
||||
renderFields();
|
||||
localPageFields.forEach((field) => {
|
||||
renderFieldOnLayer(field);
|
||||
});
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
}, [selectedAssistantRecipient]);
|
||||
@ -550,15 +475,6 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
</EnvelopeFieldToolTip>
|
||||
)}
|
||||
|
||||
{localPageOtherRecipientFields.map((field) => (
|
||||
<EnvelopeRecipientFieldTooltip
|
||||
key={field.id}
|
||||
field={field}
|
||||
showFieldStatus={true}
|
||||
showRecipientTooltip={true}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* The element Konva will inject it's canvas into. */}
|
||||
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
||||
|
||||
|
||||
@ -7,13 +7,11 @@ import type { User } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||
|
||||
export type TemplatePageViewInformationProps = {
|
||||
userId: number;
|
||||
template: {
|
||||
userId: number;
|
||||
secondaryId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
user: Pick<User, 'id' | 'name' | 'email'>;
|
||||
@ -45,10 +43,6 @@ export const TemplatePageViewInformation = ({
|
||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||
.toRelative(),
|
||||
},
|
||||
{
|
||||
description: msg`Template ID (Legacy)`,
|
||||
value: mapSecondaryIdToTemplateId(template.secondaryId),
|
||||
},
|
||||
];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isMounted, template, userId]);
|
||||
|
||||
@ -8,7 +8,7 @@ import { SignFieldNumberDialog } from '~/components/dialogs/sign-field-number-di
|
||||
|
||||
type HandleNumberFieldClickOptions = {
|
||||
field: TFieldNumber;
|
||||
number: string | null;
|
||||
number: number | null;
|
||||
};
|
||||
|
||||
export const handleNumberFieldClick = async (
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
"@simplewebauthn/server": "^9.0.3",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"colord": "^2.9.3",
|
||||
"content-disposition": "^0.5.4",
|
||||
"framer-motion": "^10.12.8",
|
||||
"hono": "4.7.0",
|
||||
"hono-rate-limiter": "^0.4.2",
|
||||
@ -88,7 +87,6 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@simplewebauthn/types": "^9.0.1",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/node": "^20",
|
||||
@ -106,5 +104,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "2.0.6"
|
||||
"version": "2.0.0"
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { type DocumentDataType, DocumentStatus } from '@prisma/client';
|
||||
import contentDisposition from 'content-disposition';
|
||||
import { type Context } from 'hono';
|
||||
|
||||
import { sha256 } from '@documenso/lib/universal/crypto';
|
||||
@ -35,7 +34,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
||||
|
||||
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
|
||||
|
||||
if (c.req.header('If-None-Match') === etag && !isDownload) {
|
||||
if (c.req.header('If-None-Match') === etag) {
|
||||
return c.body(null, 304);
|
||||
}
|
||||
|
||||
@ -59,6 +58,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
||||
if (status === DocumentStatus.COMPLETED) {
|
||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
} else {
|
||||
// Set a tiny 1 minute cache, with must-revalidate to ensure the client always checks for updates.
|
||||
c.header('Cache-Control', 'public, max-age=0, must-revalidate');
|
||||
}
|
||||
}
|
||||
@ -69,7 +69,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||
const filename = `${baseTitle}${suffix}`;
|
||||
|
||||
c.header('Content-Disposition', contentDisposition(filename));
|
||||
c.header('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
|
||||
// For downloads, prevent caching to ensure fresh data
|
||||
c.header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
|
||||
28
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.0",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@ -19,6 +19,7 @@
|
||||
"inngest-cli": "^0.29.1",
|
||||
"luxon": "^3.5.0",
|
||||
"mupdf": "^1.0.0",
|
||||
"pdf2json": "^4.0.0",
|
||||
"react": "^18",
|
||||
"typescript": "5.6.2",
|
||||
"zod": "^3.25.76"
|
||||
@ -100,7 +101,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@cantoo/pdf-lib": "^2.5.2",
|
||||
"@documenso/api": "*",
|
||||
@ -129,7 +130,6 @@
|
||||
"@simplewebauthn/server": "^9.0.3",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"colord": "^2.9.3",
|
||||
"content-disposition": "^0.5.4",
|
||||
"framer-motion": "^10.12.8",
|
||||
"hono": "4.7.0",
|
||||
"hono-rate-limiter": "^0.4.2",
|
||||
@ -176,7 +176,6 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@simplewebauthn/types": "^9.0.1",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/formidable": "^2.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/node": "^20",
|
||||
@ -12317,13 +12316,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/content-disposition": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz",
|
||||
"integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cross-spawn": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",
|
||||
@ -27497,6 +27489,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdf2json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pdf2json/-/pdf2json-4.0.0.tgz",
|
||||
"integrity": "sha512-WkezNsLK8sGpuFC7+PPP0DsXROwdoOxmXPBTtUWWkCwCi/Vi97MRC52Ly6FWIJjOKIywpm/L2oaUgSrmtU+7ZQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"pdf2json": "bin/pdf2json.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "3.11.174",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.0",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
@ -85,6 +85,7 @@
|
||||
"inngest-cli": "^0.29.1",
|
||||
"luxon": "^3.5.0",
|
||||
"mupdf": "^1.0.0",
|
||||
"pdf2json": "^4.0.0",
|
||||
"react": "^18",
|
||||
"typescript": "5.6.2",
|
||||
"zod": "^3.25.76"
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { FieldType } from '@prisma/client';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
|
||||
@ -15,66 +13,11 @@ export type FieldTestData = TFieldAndMeta & {
|
||||
signature?: string;
|
||||
};
|
||||
|
||||
export const signatureBase64Demo = `data:image/png;base64,${fs.readFileSync(
|
||||
path.join(__dirname, '../../../packages/assets/', 'logo_icon.png'),
|
||||
'base64',
|
||||
)}`;
|
||||
|
||||
const columnWidth = 19.125;
|
||||
const fullColumnWidth = 57.37499999999998;
|
||||
const rowHeight = 6.7;
|
||||
const rowPadding = 0;
|
||||
|
||||
const calculatePositionPageOne = (
|
||||
row: number,
|
||||
column: number,
|
||||
width: 'full' | 'column' = 'column',
|
||||
) => {
|
||||
const alignmentGridStartX = 31;
|
||||
const alignmentGridStartY = 19;
|
||||
|
||||
return {
|
||||
height: rowHeight,
|
||||
width: width === 'full' ? fullColumnWidth : columnWidth,
|
||||
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
|
||||
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
|
||||
};
|
||||
};
|
||||
|
||||
const calculatePositionPageTwo = (
|
||||
row: number,
|
||||
column: number,
|
||||
width: 'full' | 'column' = 'column',
|
||||
) => {
|
||||
const alignmentGridStartX = 31;
|
||||
const alignmentGridStartY = 16.35;
|
||||
|
||||
return {
|
||||
height: rowHeight,
|
||||
width: width === 'full' ? fullColumnWidth : columnWidth,
|
||||
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
|
||||
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
|
||||
};
|
||||
};
|
||||
|
||||
const calculatePositionPageThree = (
|
||||
row: number,
|
||||
column: number,
|
||||
width: 'full' | 'column' = 'column',
|
||||
rowQuantity: number = 1,
|
||||
) => {
|
||||
const alignmentGridStartX = 31;
|
||||
const alignmentGridStartY = 16.4;
|
||||
|
||||
const rowHeight = 6.8;
|
||||
|
||||
return {
|
||||
height: rowHeight * rowQuantity,
|
||||
width: width === 'full' ? fullColumnWidth : columnWidth,
|
||||
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
|
||||
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
|
||||
};
|
||||
};
|
||||
const alignmentGridStartX = 31;
|
||||
const alignmentGridStartY = 19.02;
|
||||
|
||||
export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
/**
|
||||
@ -88,7 +31,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'email',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(0, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'admin@documenso.com',
|
||||
},
|
||||
{
|
||||
@ -98,7 +44,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'email',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(0, 1),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'admin@documenso.com',
|
||||
},
|
||||
{
|
||||
@ -109,7 +58,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'email',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(0, 2),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'admin@documenso.com',
|
||||
},
|
||||
/**
|
||||
@ -123,7 +75,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'name',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(1, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'John Doe',
|
||||
},
|
||||
{
|
||||
@ -133,7 +88,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'name',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(1, 1),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'John Doe',
|
||||
},
|
||||
{
|
||||
@ -144,7 +102,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'name',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(1, 2),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'John Doe',
|
||||
},
|
||||
/**
|
||||
@ -158,7 +119,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'date',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(2, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -168,7 +132,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'date',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(2, 1),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -179,7 +146,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'date',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(2, 2),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
/**
|
||||
@ -193,7 +163,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'text',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(3, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -203,7 +176,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'text',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(3, 1),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -214,7 +190,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'text',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(3, 2),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
/**
|
||||
@ -228,7 +207,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'number',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(4, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -238,7 +220,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'number',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(4, 1),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -249,7 +234,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'number',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(4, 2),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '123456789',
|
||||
},
|
||||
/**
|
||||
@ -263,7 +251,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'initials',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(5, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'JD',
|
||||
},
|
||||
{
|
||||
@ -273,7 +264,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'initials',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(5, 1),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'JD',
|
||||
},
|
||||
{
|
||||
@ -284,7 +278,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'initials',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(5, 2),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'JD',
|
||||
},
|
||||
/**
|
||||
@ -302,7 +299,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
],
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(6, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '0',
|
||||
},
|
||||
{
|
||||
@ -312,12 +312,15 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'radio',
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
],
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(6, 1),
|
||||
customText: '',
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '2',
|
||||
},
|
||||
{
|
||||
type: FieldType.RADIO,
|
||||
@ -327,12 +330,15 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'radio',
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
],
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(6, 2),
|
||||
customText: '1',
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '',
|
||||
},
|
||||
/**
|
||||
* Row 8 Checkbox
|
||||
@ -349,7 +355,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
],
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(7, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: toCheckboxCustomText([0]),
|
||||
},
|
||||
{
|
||||
@ -359,12 +368,15 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'checkbox',
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
],
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(7, 1),
|
||||
customText: '',
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: toCheckboxCustomText([1]),
|
||||
},
|
||||
{
|
||||
type: FieldType.CHECKBOX,
|
||||
@ -374,12 +386,15 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'checkbox',
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
],
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(7, 2),
|
||||
customText: toCheckboxCustomText([1]),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '',
|
||||
},
|
||||
/**
|
||||
* Row 8 Dropdown
|
||||
@ -392,7 +407,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'dropdown',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(8, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'Option 1',
|
||||
},
|
||||
{
|
||||
@ -402,7 +420,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'dropdown',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(8, 1),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'Option 1',
|
||||
},
|
||||
{
|
||||
@ -413,7 +434,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'dropdown',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(8, 2),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: 'Option 1',
|
||||
},
|
||||
/**
|
||||
@ -426,7 +450,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'signature',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(9, 0),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '',
|
||||
signature: 'My Signature',
|
||||
},
|
||||
@ -436,7 +463,10 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'signature',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(9, 1),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '',
|
||||
signature: 'My Signature',
|
||||
},
|
||||
@ -447,295 +477,22 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'signature',
|
||||
},
|
||||
page: 1,
|
||||
...calculatePositionPageOne(9, 2),
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '',
|
||||
signature: 'My Signature',
|
||||
},
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 2
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
// TEXT GRID ROW 1
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'left',
|
||||
type: 'text',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(0, 0),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'center',
|
||||
type: 'text',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(0, 1),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'right',
|
||||
type: 'text',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(0, 2),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
// TEXT GRID ROW 2
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'left',
|
||||
type: 'text',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(1, 0),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'center',
|
||||
type: 'text',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(1, 1),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'right',
|
||||
type: 'text',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(1, 2),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
// TEXT GRID ROW 3
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'left',
|
||||
type: 'text',
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(2, 0),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'center',
|
||||
type: 'text',
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(2, 1),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
textAlign: 'right',
|
||||
type: 'text',
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(2, 2),
|
||||
customText: 'SOME TEXT',
|
||||
},
|
||||
// NUMBER GRID ROW 1
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'left',
|
||||
type: 'number',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(3, 0),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'center',
|
||||
type: 'number',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(3, 1),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'right',
|
||||
type: 'number',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(3, 2),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
// NUMBER GRID ROW 2
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'left',
|
||||
type: 'number',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(4, 0),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'center',
|
||||
type: 'number',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(4, 1),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'right',
|
||||
type: 'number',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(4, 2),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
// NUMBER GRID ROW 3
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'left',
|
||||
type: 'number',
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(5, 0),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'center',
|
||||
type: 'number',
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(5, 1),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
textAlign: 'right',
|
||||
type: 'number',
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(5, 2),
|
||||
customText: '123456789123456789',
|
||||
},
|
||||
// Text combing
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
type: 'text',
|
||||
verticalAlign: 'middle',
|
||||
letterSpacing: 32,
|
||||
characterLimit: 9,
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(6, 0, 'full'),
|
||||
positionX: calculatePositionPageTwo(6, 0, 'full').positionX + 1.75,
|
||||
width: calculatePositionPageTwo(6, 0, 'full').width + 1.75,
|
||||
customText: 'HEY HEY 1',
|
||||
},
|
||||
// Number combing
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
type: 'number',
|
||||
verticalAlign: 'middle',
|
||||
letterSpacing: 32,
|
||||
},
|
||||
page: 2,
|
||||
...calculatePositionPageTwo(7, 0, 'full'),
|
||||
positionX: calculatePositionPageTwo(7, 0, 'full').positionX + 1.75,
|
||||
width: calculatePositionPageTwo(7, 0, 'full').width + 1.75,
|
||||
|
||||
customText: '123456789',
|
||||
},
|
||||
/**
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*
|
||||
* PAGE 2 TEXT MULTILINE
|
||||
*
|
||||
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||
*/
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
verticalAlign: 'top',
|
||||
textAlign: 'left',
|
||||
lineHeight: 2.24,
|
||||
type: 'text',
|
||||
},
|
||||
page: 3,
|
||||
...calculatePositionPageThree(0, 0, 'full', 3),
|
||||
customText:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
verticalAlign: 'middle',
|
||||
textAlign: 'center',
|
||||
lineHeight: 2.24,
|
||||
type: 'text',
|
||||
},
|
||||
page: 3,
|
||||
...calculatePositionPageThree(3, 0, 'full', 3),
|
||||
customText:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
verticalAlign: 'bottom',
|
||||
textAlign: 'right',
|
||||
lineHeight: 2.24,
|
||||
type: 'text',
|
||||
},
|
||||
page: 3,
|
||||
...calculatePositionPageThree(6, 0, 'full', 3),
|
||||
customText:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const formatAlignmentTestFields = ALIGNMENT_TEST_FIELDS.map((field, index) => {
|
||||
const row = Math.floor(index / 3);
|
||||
const column = index % 3;
|
||||
|
||||
return {
|
||||
...field,
|
||||
positionX: alignmentGridStartX + column * columnWidth,
|
||||
positionY: alignmentGridStartY + row * rowHeight,
|
||||
};
|
||||
});
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
} from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
||||
|
||||
import type { FieldTestData } from './field-alignment-pdf';
|
||||
import { signatureBase64Demo } from './field-alignment-pdf';
|
||||
|
||||
const columnWidth = 20.1;
|
||||
const fullColumnWidth = 75.8;
|
||||
@ -38,7 +37,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
page: 2,
|
||||
...calculatePosition(0, 0),
|
||||
customText: '',
|
||||
signature: signatureBase64Demo,
|
||||
signature: 'My Signature',
|
||||
},
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
@ -48,7 +47,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
page: 2,
|
||||
...calculatePosition(1, 0),
|
||||
customText: '',
|
||||
signature: signatureBase64Demo,
|
||||
signature: 'My Signature',
|
||||
},
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
@ -68,7 +67,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
page: 2,
|
||||
...calculatePosition(3, 0),
|
||||
customText: '',
|
||||
signature: 'My Signature super overflow maybe',
|
||||
signature: 'My Signature',
|
||||
},
|
||||
|
||||
/**
|
||||
@ -81,7 +80,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(0, 0, 'full'),
|
||||
customText: 'Hello world, this is some random text that I have written here',
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -90,7 +89,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(1, 0),
|
||||
customText: 'Some text that should overflow correctly',
|
||||
customText: '123456789123456789123456789123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -110,7 +109,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(3, 0),
|
||||
customText: 'Input should have a placeholder text when clicked',
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -120,7 +119,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(3, 1),
|
||||
customText: 'Should have a label during editing and signing',
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -130,7 +129,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(3, 2),
|
||||
customText: '',
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -140,19 +139,20 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(4, 0),
|
||||
customText: 'This is a required field',
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
type: 'text',
|
||||
readOnly: true,
|
||||
text: 'Some Readonly Value',
|
||||
text: 'Readonly Value',
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(4, 1),
|
||||
customText: '',
|
||||
customText: 'Readonly Value',
|
||||
},
|
||||
|
||||
/**
|
||||
* PAGE 4 NUMBER
|
||||
*/
|
||||
@ -220,7 +220,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
type: 'number',
|
||||
value: '123456789',
|
||||
value: '123',
|
||||
},
|
||||
page: 4,
|
||||
...calculatePosition(3, 2),
|
||||
@ -241,11 +241,10 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
type: 'number',
|
||||
readOnly: true,
|
||||
value: '123456789',
|
||||
},
|
||||
page: 4,
|
||||
...calculatePosition(4, 1),
|
||||
customText: '',
|
||||
customText: '123456789',
|
||||
},
|
||||
|
||||
/**
|
||||
@ -273,8 +272,8 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'radio',
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
{ id: 3, checked: true, value: 'Option 3' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
{ id: 3, checked: false, value: 'Option 3' },
|
||||
],
|
||||
},
|
||||
page: 5,
|
||||
@ -286,7 +285,6 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
direction: 'vertical',
|
||||
type: 'radio',
|
||||
required: true,
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
@ -295,18 +293,17 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 5,
|
||||
...calculatePosition(2, 0),
|
||||
customText: '2',
|
||||
customText: '',
|
||||
},
|
||||
{
|
||||
type: FieldType.RADIO,
|
||||
fieldMeta: {
|
||||
direction: 'vertical',
|
||||
type: 'radio',
|
||||
readOnly: true,
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
{ id: 3, checked: true, value: 'Option 3' },
|
||||
{ id: 3, checked: false, value: 'Option 3' },
|
||||
],
|
||||
},
|
||||
page: 5,
|
||||
@ -341,7 +338,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
{ id: 3, checked: false, value: 'Option 3' },
|
||||
{ id: 2, checked: true, value: 'Option 3' },
|
||||
],
|
||||
},
|
||||
page: 6,
|
||||
@ -361,7 +358,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 6,
|
||||
...calculatePosition(2, 0),
|
||||
customText: toCheckboxCustomText([2]),
|
||||
customText: '',
|
||||
},
|
||||
{
|
||||
type: FieldType.CHECKBOX,
|
||||
@ -371,7 +368,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
readOnly: true,
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
],
|
||||
},
|
||||
page: 6,
|
||||
@ -448,11 +445,11 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
|
||||
type: 'dropdown',
|
||||
defaultValue: 'Option 2',
|
||||
defaultValue: 'Option 1',
|
||||
},
|
||||
page: 7,
|
||||
...calculatePosition(1, 0),
|
||||
customText: 'Option 2',
|
||||
customText: 'Option 1',
|
||||
},
|
||||
{
|
||||
type: FieldType.DROPDOWN,
|
||||
@ -463,14 +460,13 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 7,
|
||||
...calculatePosition(2, 0),
|
||||
customText: 'Option 3',
|
||||
customText: 'Option 1',
|
||||
},
|
||||
{
|
||||
type: FieldType.DROPDOWN,
|
||||
fieldMeta: {
|
||||
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
|
||||
type: 'dropdown',
|
||||
defaultValue: 'Option 1',
|
||||
readOnly: true,
|
||||
},
|
||||
page: 7,
|
||||
|
||||
@ -27,7 +27,7 @@ import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/en
|
||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
||||
|
||||
import { ALIGNMENT_TEST_FIELDS } from '../../../constants/field-alignment-pdf';
|
||||
import { formatAlignmentTestFields } from '../../../constants/field-alignment-pdf';
|
||||
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
@ -490,7 +490,7 @@ test.describe('API V2 Envelopes', () => {
|
||||
// Step 6: Create fields for first PDF (alignment fields)
|
||||
const alignmentFieldsRequest = {
|
||||
envelopeId: createdEnvelope.id,
|
||||
data: ALIGNMENT_TEST_FIELDS.map((field) => ({
|
||||
data: formatAlignmentTestFields.map((field) => ({
|
||||
recipientId,
|
||||
envelopeItemId: alignmentItem.id,
|
||||
type: field.type,
|
||||
@ -547,7 +547,7 @@ test.describe('API V2 Envelopes', () => {
|
||||
expect(finalEnvelope.envelopeItems.length).toBe(2);
|
||||
expect(finalEnvelope.recipients.length).toBe(1);
|
||||
expect(finalEnvelope.fields.length).toBe(
|
||||
ALIGNMENT_TEST_FIELDS.length + FIELD_META_TEST_FIELDS.length,
|
||||
formatAlignmentTestFields.length + FIELD_META_TEST_FIELDS.length,
|
||||
);
|
||||
expect(finalEnvelope.title).toBe('Envelope Full Field Test');
|
||||
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
|
||||
|
||||
@ -0,0 +1,129 @@
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
const SINGLE_PLACEHOLDER_PDF_PATH = path.join(
|
||||
__dirname,
|
||||
'../../../assets/project-proposal-single-recipient.pdf',
|
||||
);
|
||||
|
||||
const MULTIPLE_PLACEHOLDER_PDF_PATH = path.join(
|
||||
__dirname,
|
||||
'../../../assets/project-proposal-multiple-fields-and-recipients.pdf',
|
||||
);
|
||||
|
||||
const setupUserAndSignIn = async (page: Page) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
return { user, team };
|
||||
};
|
||||
|
||||
const uploadPdfAndContinue = async (page: Page, pdfPath: string, continueClicks: number = 1) => {
|
||||
const fileInput = page.locator('input[type="file"]').nth(1);
|
||||
await fileInput.waitFor({ state: 'attached' });
|
||||
await fileInput.setInputFiles(pdfPath);
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
for (let i = 0; i < continueClicks; i++) {
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
};
|
||||
|
||||
test.describe('PDF Placeholders with single recipient', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, SINGLE_PLACEHOLDER_PDF_PATH, 1);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
await expect(page.getByPlaceholder('Email')).toHaveValue('recipient.1@documenso.com');
|
||||
await expect(page.getByPlaceholder('Name')).toHaveValue('Recipient 1');
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically place fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, SINGLE_PLACEHOLDER_PDF_PATH, 2);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await expect(page.locator('[data-field-type="SIGNATURE"]')).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="EMAIL"]')).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="NAME"]')).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="TEXT"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically configure fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, SINGLE_PLACEHOLDER_PDF_PATH, 2);
|
||||
|
||||
await page.getByText('Text').nth(1).click();
|
||||
await page.getByRole('button', { name: 'Advanced settings' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Advanced settings' })).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Required field$/ })
|
||||
.getByRole('switch'),
|
||||
).toBeChecked();
|
||||
|
||||
await expect(page.getByRole('combobox')).toHaveText('Right');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PDF Placeholders with multiple recipients', () => {
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, MULTIPLE_PLACEHOLDER_PDF_PATH, 1);
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').first()).toHaveValue(
|
||||
'recipient.1@documenso.com',
|
||||
);
|
||||
await expect(page.getByLabel('Name').first()).toHaveValue('Recipient 1');
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').nth(1)).toHaveValue(
|
||||
'recipient.2@documenso.com',
|
||||
);
|
||||
await expect(page.getByLabel('Name').nth(1)).toHaveValue('Recipient 2');
|
||||
|
||||
await expect(page.getByTestId('signer-email-input').nth(2)).toHaveValue(
|
||||
'recipient.3@documenso.com',
|
||||
);
|
||||
await expect(page.getByLabel('Name').nth(2)).toHaveValue('Recipient 3');
|
||||
});
|
||||
|
||||
test('[AUTO_PLACING_FIELDS]: should automatically create fields from PDF placeholders', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupUserAndSignIn(page);
|
||||
await uploadPdfAndContinue(page, MULTIPLE_PLACEHOLDER_PDF_PATH, 2);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await expect(page.locator('[data-field-type="SIGNATURE"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="SIGNATURE"]').nth(1)).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="SIGNATURE"]').nth(2)).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="EMAIL"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="EMAIL"]').nth(1)).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="NAME"]')).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="TEXT"]')).toBeVisible();
|
||||
await expect(page.locator('[data-field-type="NUMBER"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@ -21,7 +21,7 @@ import pixelMatch from 'pixelmatch';
|
||||
import { PNG } from 'pngjs';
|
||||
import type { TestInfo } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
||||
@ -29,218 +29,26 @@ import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '../../../trpc/server/envelope-router/create-envelope.types';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
|
||||
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
|
||||
import { RecipientRole } from '../../../prisma/generated/types';
|
||||
import { FIELD_META_TEST_FIELDS } from '../../constants/field-meta-pdf';
|
||||
import { ALIGNMENT_TEST_FIELDS } from '../../constants/field-alignment-pdf';
|
||||
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
|
||||
import { isBase64Image } from '../../../lib/constants/signatures';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
test.skip('seed alignment test document', async ({ page }) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
email: 'example@documenso.com',
|
||||
},
|
||||
include: {
|
||||
ownedOrganisations: {
|
||||
include: {
|
||||
teams: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userId = user.id;
|
||||
const teamId = user.ownedOrganisations[0].teams[0].id;
|
||||
|
||||
await seedAlignmentTestDocument({
|
||||
userId,
|
||||
teamId,
|
||||
recipientName: user.name || '',
|
||||
recipientEmail: user.email,
|
||||
insertFields: false,
|
||||
status: DocumentStatus.DRAFT,
|
||||
});
|
||||
});
|
||||
|
||||
test('field placement visual regression', async ({ page, request }, testInfo) => {
|
||||
test.skip('field placement visual regression', async ({ page }, testInfo) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const { token } = await createApiToken({
|
||||
const envelope = await seedAlignmentTestDocument({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
recipientName: user.name || '',
|
||||
recipientEmail: user.email,
|
||||
insertFields: true,
|
||||
status: DocumentStatus.PENDING,
|
||||
});
|
||||
|
||||
// Step 1: Create initial envelope with Prisma (with first envelope item)
|
||||
const alignmentPdf = fs.readFileSync(
|
||||
path.join(__dirname, '../../../../assets/field-font-alignment.pdf'),
|
||||
);
|
||||
const token = envelope.recipients[0].token;
|
||||
|
||||
const fieldMetaPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/field-meta.pdf'));
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const fieldMetaFields = FIELD_META_TEST_FIELDS.map((field) => ({
|
||||
identifier: 'field-meta',
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
fieldMeta: field.fieldMeta,
|
||||
}));
|
||||
|
||||
const alignmentFields = ALIGNMENT_TEST_FIELDS.map((field) => ({
|
||||
identifier: 'alignment-pdf',
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
fieldMeta: field.fieldMeta,
|
||||
}));
|
||||
|
||||
const createEnvelopePayload: TCreateEnvelopePayload = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Envelope Full Field Test',
|
||||
recipients: [
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name || '',
|
||||
role: RecipientRole.SIGNER,
|
||||
fields: [...fieldMetaFields, ...alignmentFields],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
formData.append('payload', JSON.stringify(createEnvelopePayload));
|
||||
|
||||
formData.append('files', new File([alignmentPdf], 'alignment-pdf', { type: 'application/pdf' }));
|
||||
formData.append('files', new File([fieldMetaPdf], 'field-meta', { type: 'application/pdf' }));
|
||||
|
||||
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(createEnvelopeRequest.ok()).toBeTruthy();
|
||||
expect(createEnvelopeRequest.status()).toBe(200);
|
||||
|
||||
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
|
||||
|
||||
const envelope = await prisma.envelope.findUniqueOrThrow({
|
||||
where: {
|
||||
id: createdEnvelopeId,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
envelopeItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientId = envelope.recipients[0].id;
|
||||
const alignmentItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 1);
|
||||
const fieldMetaItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 2);
|
||||
|
||||
expect(recipientId).toBeDefined();
|
||||
expect(alignmentItem).toBeDefined();
|
||||
expect(fieldMetaItem).toBeDefined();
|
||||
|
||||
if (!alignmentItem || !fieldMetaItem) {
|
||||
throw new Error('Envelope items not found');
|
||||
}
|
||||
|
||||
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
} satisfies TDistributeEnvelopeRequest,
|
||||
});
|
||||
|
||||
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
|
||||
|
||||
const uninsertedFields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
inserted: false,
|
||||
},
|
||||
include: {
|
||||
envelopeItem: {
|
||||
select: {
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
uninsertedFields.map(async (field) => {
|
||||
let foundField = ALIGNMENT_TEST_FIELDS.find(
|
||||
(f) =>
|
||||
field.page === f.page &&
|
||||
field.envelopeItem.title === 'alignment-pdf' &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||
);
|
||||
|
||||
if (!foundField) {
|
||||
foundField = FIELD_META_TEST_FIELDS.find(
|
||||
(f) =>
|
||||
field.page === f.page &&
|
||||
field.envelopeItem.title === 'field-meta' &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||
);
|
||||
}
|
||||
|
||||
if (!foundField) {
|
||||
throw new Error('Field not found');
|
||||
}
|
||||
|
||||
await prisma.field.update({
|
||||
where: {
|
||||
id: field.id,
|
||||
},
|
||||
data: {
|
||||
inserted: true,
|
||||
customText: foundField.customText,
|
||||
signature: foundField.signature
|
||||
? {
|
||||
create: {
|
||||
recipientId: envelope.recipients[0].id,
|
||||
signatureImageAsBase64: isBase64Image(foundField.signature)
|
||||
? foundField.signature
|
||||
: null,
|
||||
typedSignature: isBase64Image(foundField.signature) ? null : foundField.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const recipientToken = envelope.recipients[0].token;
|
||||
const signUrl = `/sign/${recipientToken}`;
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
@ -289,7 +97,7 @@ test('field placement visual regression', async ({ page, request }, testInfo) =>
|
||||
const documentUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'download',
|
||||
envelopeItem: item,
|
||||
token: recipientToken,
|
||||
token,
|
||||
version: 'signed',
|
||||
});
|
||||
|
||||
@ -481,7 +289,7 @@ const compareSignedPdfWithImages = async ({
|
||||
// Expect the certificate to NOT be blank. Since the storedImage is blank.
|
||||
expect.soft(comparison).toBeGreaterThan(20000);
|
||||
} else {
|
||||
expect.soft(comparison).toBeLessThan(2);
|
||||
expect.soft(comparison).toEqual(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 117 KiB |
@ -11,7 +11,7 @@ export const validateNumberField = (
|
||||
|
||||
const { minValue, maxValue, readOnly, required, numberFormat, fontSize } = fieldMeta || {};
|
||||
|
||||
if (numberFormat && value.length > 0) {
|
||||
if (numberFormat) {
|
||||
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
|
||||
|
||||
if (!foundRegex) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
@ -63,8 +63,6 @@ type UseEditorFieldsResponse = {
|
||||
// Selected recipient
|
||||
selectedRecipient: Recipient | null;
|
||||
setSelectedRecipient: (recipientId: number | null) => void;
|
||||
|
||||
resetForm: (fields?: Field[]) => void;
|
||||
};
|
||||
|
||||
export const useEditorFields = ({
|
||||
@ -74,30 +72,24 @@ export const useEditorFields = ({
|
||||
const [selectedFieldFormId, setSelectedFieldFormId] = useState<string | null>(null);
|
||||
const [selectedRecipientId, setSelectedRecipientId] = useState<number | null>(null);
|
||||
|
||||
const generateDefaultValues = (fields?: Field[]) => {
|
||||
const formFields = (fields || envelope.fields).map(
|
||||
(field): TLocalField => ({
|
||||
id: field.id,
|
||||
formId: nanoid(),
|
||||
envelopeItemId: field.envelopeItemId,
|
||||
page: field.page,
|
||||
type: field.type,
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
recipientId: field.recipientId,
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
fields: formFields,
|
||||
};
|
||||
};
|
||||
|
||||
const form = useForm<TEditorFieldsFormSchema>({
|
||||
defaultValues: generateDefaultValues(),
|
||||
defaultValues: {
|
||||
fields: envelope.fields.map(
|
||||
(field): TLocalField => ({
|
||||
id: field.id,
|
||||
formId: nanoid(),
|
||||
envelopeItemId: field.envelopeItemId,
|
||||
page: field.page,
|
||||
type: field.type,
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
recipientId: field.recipientId,
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
}),
|
||||
),
|
||||
},
|
||||
resolver: zodResolver(ZEditorFieldsFormSchema),
|
||||
});
|
||||
|
||||
@ -280,10 +272,6 @@ export const useEditorFields = ({
|
||||
setSelectedRecipientId(foundRecipient?.id ?? null);
|
||||
};
|
||||
|
||||
const resetForm = (fields?: Field[]) => {
|
||||
form.reset(generateDefaultValues(fields));
|
||||
};
|
||||
|
||||
return {
|
||||
// Core state
|
||||
localFields,
|
||||
@ -307,8 +295,6 @@ export const useEditorFields = ({
|
||||
// Selected recipient
|
||||
selectedRecipient,
|
||||
setSelectedRecipient,
|
||||
|
||||
resetForm,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -107,10 +107,6 @@ export function usePageRenderer(renderFunction: RenderFunction) {
|
||||
stage: stage.current,
|
||||
pageLayer: pageLayer.current,
|
||||
});
|
||||
|
||||
void document.fonts.ready.then(function () {
|
||||
pageLayer.current?.batchDraw();
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@ -30,8 +30,6 @@ type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
|
||||
type EnvelopeRenderProviderValue = {
|
||||
getPdfBuffer: (envelopeItemId: string) => FileData | null;
|
||||
envelopeItems: EnvelopeRenderItem[];
|
||||
envelopeStatus: TEnvelope['status'];
|
||||
envelopeType: TEnvelope['type'];
|
||||
currentEnvelopeItem: EnvelopeRenderItem | null;
|
||||
setCurrentEnvelopeItem: (envelopeItemId: string) => void;
|
||||
fields: Field[];
|
||||
@ -46,7 +44,7 @@ type EnvelopeRenderProviderValue = {
|
||||
interface EnvelopeRenderProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
envelope: Pick<TEnvelope, 'envelopeItems' | 'status' | 'type'>;
|
||||
envelope: Pick<TEnvelope, 'envelopeItems'>;
|
||||
|
||||
/**
|
||||
* Optional fields which are passed down to renderers for custom rendering needs.
|
||||
@ -102,7 +100,7 @@ export const EnvelopeRenderProvider = ({
|
||||
// Indexed by documentDataId.
|
||||
const [files, setFiles] = useState<Record<string, FileData>>({});
|
||||
|
||||
const [currentItem, setCurrentItem] = useState<EnvelopeRenderItem | null>(null);
|
||||
const [currentItem, setItem] = useState<EnvelopeRenderItem | null>(null);
|
||||
|
||||
const [renderError, setRenderError] = useState<boolean>(false);
|
||||
|
||||
@ -165,15 +163,11 @@ export const EnvelopeRenderProvider = ({
|
||||
const setCurrentEnvelopeItem = (envelopeItemId: string) => {
|
||||
const foundItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId);
|
||||
|
||||
setCurrentItem(foundItem ?? null);
|
||||
setItem(foundItem ?? null);
|
||||
};
|
||||
|
||||
// Set the selected item to the first item if none is set.
|
||||
useEffect(() => {
|
||||
if (currentItem && !envelopeItems.some((item) => item.id === currentItem.id)) {
|
||||
setCurrentItem(null);
|
||||
}
|
||||
|
||||
if (!currentItem && envelopeItems.length > 0) {
|
||||
setCurrentEnvelopeItem(envelopeItems[0].id);
|
||||
}
|
||||
@ -209,8 +203,6 @@ export const EnvelopeRenderProvider = ({
|
||||
value={{
|
||||
getPdfBuffer,
|
||||
envelopeItems,
|
||||
envelopeStatus: envelope.status,
|
||||
envelopeType: envelope.type,
|
||||
currentEnvelopeItem: currentItem,
|
||||
setCurrentEnvelopeItem,
|
||||
fields: fields ?? [],
|
||||
|
||||
@ -32,7 +32,6 @@ export type JobDefinition<Name extends string = string, Schema = any> = {
|
||||
name: string;
|
||||
version: string;
|
||||
enabled?: boolean;
|
||||
optimizeParallelism?: boolean;
|
||||
trigger: {
|
||||
name: Name;
|
||||
schema?: z.ZodType<Schema>;
|
||||
|
||||
@ -40,7 +40,6 @@ export class InngestJobProvider extends BaseJobProvider {
|
||||
{
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
optimizeParallelism: job.optimizeParallelism ?? false,
|
||||
},
|
||||
{
|
||||
event: job.trigger.name,
|
||||
|
||||
@ -189,65 +189,29 @@ export const run = async ({
|
||||
settings,
|
||||
});
|
||||
|
||||
// !: The commented out code is our desired implementation but we're seemingly
|
||||
// !: running into issues with inngest parallelism in production.
|
||||
// !: Until this is resolved we will do this sequentially which is slower but
|
||||
// !: will actually work.
|
||||
// const decoratePromises: Array<Promise<{ oldDocumentDataId: string; newDocumentDataId: string }>> =
|
||||
// [];
|
||||
const newDocumentData = await Promise.all(
|
||||
envelopeItems.map(async (envelopeItem) =>
|
||||
io.runTask(`decorate-and-sign-envelope-item-${envelopeItem.id}`, async () => {
|
||||
const envelopeItemFields = envelope.envelopeItems.find(
|
||||
(item) => item.id === envelopeItem.id,
|
||||
)?.field;
|
||||
|
||||
// for (const envelopeItem of envelopeItems) {
|
||||
// const task = io.runTask(`decorate-${envelopeItem.id}`, async () => {
|
||||
// const envelopeItemFields = envelope.envelopeItems.find(
|
||||
// (item) => item.id === envelopeItem.id,
|
||||
// )?.field;
|
||||
if (!envelopeItemFields) {
|
||||
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||
}
|
||||
|
||||
// if (!envelopeItemFields) {
|
||||
// throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||
// }
|
||||
|
||||
// return decorateAndSignPdf({
|
||||
// envelope,
|
||||
// envelopeItem,
|
||||
// envelopeItemFields,
|
||||
// isRejected,
|
||||
// rejectionReason,
|
||||
// certificateData,
|
||||
// auditLogData,
|
||||
// });
|
||||
// });
|
||||
|
||||
// decoratePromises.push(task);
|
||||
// }
|
||||
|
||||
// const newDocumentData = await Promise.all(decoratePromises);
|
||||
|
||||
// TODO: Remove once parallelization is working
|
||||
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
|
||||
|
||||
for (const envelopeItem of envelopeItems) {
|
||||
const result = await io.runTask(`decorate-${envelopeItem.id}`, async () => {
|
||||
const envelopeItemFields = envelope.envelopeItems.find(
|
||||
(item) => item.id === envelopeItem.id,
|
||||
)?.field;
|
||||
|
||||
if (!envelopeItemFields) {
|
||||
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||
}
|
||||
|
||||
return decorateAndSignPdf({
|
||||
envelope,
|
||||
envelopeItem,
|
||||
envelopeItemFields,
|
||||
isRejected,
|
||||
rejectionReason,
|
||||
certificateData,
|
||||
auditLogData,
|
||||
});
|
||||
});
|
||||
|
||||
newDocumentData.push(result);
|
||||
}
|
||||
return decorateAndSignPdf({
|
||||
envelope,
|
||||
envelopeItem,
|
||||
envelopeItemFields,
|
||||
isRejected,
|
||||
rejectionReason,
|
||||
certificateData,
|
||||
auditLogData,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const postHog = PostHogServerClient();
|
||||
|
||||
|
||||
@ -18,7 +18,6 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
|
||||
id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
||||
name: 'Seal Document',
|
||||
version: '1.0.0',
|
||||
optimizeParallelism: true,
|
||||
trigger: {
|
||||
name: SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
||||
schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DocumentData, Envelope, EnvelopeItem, Field } from '@prisma/client';
|
||||
import type { DocumentData, Envelope, EnvelopeItem } from '@prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
@ -24,9 +24,7 @@ import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZFieldAndMetaSchema,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
@ -184,19 +182,80 @@ export const sendDocument = async ({
|
||||
// Validate and autoinsert fields for V2 envelopes.
|
||||
if (envelope.internalVersion === 2) {
|
||||
for (const unknownField of envelope.fields) {
|
||||
const recipient = envelope.recipients.find((r) => r.id === unknownField.recipientId);
|
||||
const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Recipient not found',
|
||||
if (parsedField.error) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'One or more fields have invalid metadata. Error: ' + parsedField.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const fieldToAutoInsert = extractFieldAutoInsertValues(unknownField);
|
||||
const field = parsedField.data;
|
||||
const fieldId = unknownField.id;
|
||||
|
||||
// Only auto-insert fields if the recipient has not been sent the document yet.
|
||||
if (fieldToAutoInsert && recipient.sendStatus !== SendStatus.SENT) {
|
||||
fieldsToAutoInsert.push(fieldToAutoInsert);
|
||||
if (field.type === FieldType.RADIO) {
|
||||
const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
const checkedItemIndex = values.findIndex((value) => value.checked);
|
||||
|
||||
if (checkedItemIndex !== -1) {
|
||||
fieldsToAutoInsert.push({
|
||||
fieldId,
|
||||
customText: toRadioCustomText(checkedItemIndex),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DROPDOWN) {
|
||||
const { defaultValue, values = [] } = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
if (defaultValue && values.some((value) => value.value === defaultValue)) {
|
||||
fieldsToAutoInsert.push({
|
||||
fieldId,
|
||||
customText: defaultValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.CHECKBOX) {
|
||||
const {
|
||||
values = [],
|
||||
validationRule,
|
||||
validationLength,
|
||||
} = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
const checkedIndices: number[] = [];
|
||||
|
||||
values.forEach((value, i) => {
|
||||
if (value.checked) {
|
||||
checkedIndices.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
let isValid = true;
|
||||
|
||||
if (validationRule && validationLength) {
|
||||
const validation = checkboxValidationSigns.find((sign) => sign.label === validationRule);
|
||||
|
||||
if (!validation) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Invalid checkbox validation rule',
|
||||
});
|
||||
}
|
||||
|
||||
isValid = validateCheckboxLength(
|
||||
checkedIndices.length,
|
||||
validation.value,
|
||||
validationLength,
|
||||
);
|
||||
}
|
||||
|
||||
if (isValid && checkedIndices.length > 0) {
|
||||
fieldsToAutoInsert.push({
|
||||
fieldId,
|
||||
customText: toCheckboxCustomText(checkedIndices),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -216,7 +275,6 @@ export const sendDocument = async ({
|
||||
if (envelope.internalVersion === 2) {
|
||||
const autoInsertedFields = await Promise.all(
|
||||
fieldsToAutoInsert.map(async (field) => {
|
||||
// Warning: Only auto-insert fields if the recipient has not been sent the document yet.
|
||||
return await tx.field.update({
|
||||
where: {
|
||||
id: field.fieldId,
|
||||
@ -329,113 +387,3 @@ const injectFormValuesIntoDocument = async (
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the auto insertion values for a given field.
|
||||
*
|
||||
* If field is not auto insertable, returns `null`.
|
||||
*/
|
||||
export const extractFieldAutoInsertValues = (
|
||||
unknownField: Field,
|
||||
): { fieldId: number; customText: string } | null => {
|
||||
const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
|
||||
|
||||
if (parsedField.error) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'One or more fields have invalid metadata. Error: ' + parsedField.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const field = parsedField.data;
|
||||
const fieldId = unknownField.id;
|
||||
|
||||
// Auto insert text fields with prefilled values.
|
||||
if (field.type === FieldType.TEXT) {
|
||||
const { text } = ZTextFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
if (text) {
|
||||
return {
|
||||
fieldId,
|
||||
customText: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Auto insert number fields with prefilled values.
|
||||
if (field.type === FieldType.NUMBER) {
|
||||
const { value } = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
if (value) {
|
||||
return {
|
||||
fieldId,
|
||||
customText: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Auto insert radio fields with the pre-checked value.
|
||||
if (field.type === FieldType.RADIO) {
|
||||
const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
const checkedItemIndex = values.findIndex((value) => value.checked);
|
||||
|
||||
if (checkedItemIndex !== -1) {
|
||||
return {
|
||||
fieldId,
|
||||
customText: toRadioCustomText(checkedItemIndex),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Auto insert dropdown fields with the default value.
|
||||
if (field.type === FieldType.DROPDOWN) {
|
||||
const { defaultValue, values = [] } = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
if (defaultValue && values.some((value) => value.value === defaultValue)) {
|
||||
return {
|
||||
fieldId,
|
||||
customText: defaultValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Auto insert checkbox fields with the pre-checked values.
|
||||
if (field.type === FieldType.CHECKBOX) {
|
||||
const {
|
||||
values = [],
|
||||
validationRule,
|
||||
validationLength,
|
||||
} = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
const checkedIndices: number[] = [];
|
||||
|
||||
values.forEach((value, i) => {
|
||||
if (value.checked) {
|
||||
checkedIndices.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
let isValid = true;
|
||||
|
||||
if (validationRule && validationLength) {
|
||||
const validation = checkboxValidationSigns.find((sign) => sign.label === validationRule);
|
||||
|
||||
if (!validation) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Invalid checkbox validation rule',
|
||||
});
|
||||
}
|
||||
|
||||
isValid = validateCheckboxLength(checkedIndices.length, validation.value, validationLength);
|
||||
}
|
||||
|
||||
if (isValid && checkedIndices.length > 0) {
|
||||
return {
|
||||
fieldId,
|
||||
customText: toCheckboxCustomText(checkedIndices),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { insertFieldsFromPlaceholdersInPDF } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
@ -256,7 +257,7 @@ export const createEnvelope = async ({
|
||||
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const createdEnvelope = await prisma.$transaction(async (tx) => {
|
||||
const envelope = await tx.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
@ -376,8 +377,12 @@ export const createEnvelope = async ({
|
||||
recipients: true,
|
||||
fields: true,
|
||||
folder: true,
|
||||
envelopeItems: true,
|
||||
envelopeAttachments: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -413,4 +418,51 @@ export const createEnvelope = async ({
|
||||
|
||||
return createdEnvelope;
|
||||
});
|
||||
|
||||
for (const envelopeItem of createdEnvelope.envelopeItems) {
|
||||
const buffer = await getFileServerSide(envelopeItem.documentData);
|
||||
|
||||
// Use normalized PDF if normalizePdf was true, otherwise use original
|
||||
const pdfToProcess = normalizePdf
|
||||
? await makeNormalizedPdf(Buffer.from(buffer))
|
||||
: Buffer.from(buffer);
|
||||
|
||||
await insertFieldsFromPlaceholdersInPDF(
|
||||
pdfToProcess,
|
||||
userId,
|
||||
teamId,
|
||||
{
|
||||
type: 'envelopeId',
|
||||
id: createdEnvelope.id,
|
||||
},
|
||||
requestMetadata,
|
||||
envelopeItem.id,
|
||||
);
|
||||
}
|
||||
|
||||
const finalEnvelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: createdEnvelope.id,
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
folder: true,
|
||||
envelopeAttachments: true,
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!finalEnvelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
return finalEnvelope;
|
||||
};
|
||||
|
||||
@ -6,7 +6,6 @@ import { prisma } from '@documenso/prisma';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { extractFieldAutoInsertValues } from '../document/send-document';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
||||
import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
||||
@ -145,19 +144,6 @@ export const getEnvelopeForDirectTemplateSigning = async ({
|
||||
recipient: {
|
||||
...recipient,
|
||||
directToken: envelope.directLink?.token || '',
|
||||
fields: recipient.fields.map((field) => {
|
||||
const autoInsertValue = extractFieldAutoInsertValues(field);
|
||||
|
||||
if (!autoInsertValue) {
|
||||
return field;
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
inserted: true,
|
||||
customText: autoInsertValue.customText,
|
||||
};
|
||||
}),
|
||||
},
|
||||
recipientSignature: null,
|
||||
isRecipientsTurn: true,
|
||||
|
||||
@ -11,7 +11,7 @@ import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import type { TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field';
|
||||
import { ZFieldSchema } from '../../types/field';
|
||||
import { ZRecipientLiteSchema } from '../../types/recipient';
|
||||
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
@ -63,11 +63,9 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
rejectionReason: true,
|
||||
})
|
||||
.extend({
|
||||
fields: ZEnvelopeFieldSchema.extend({
|
||||
signature: SignatureSchema.pick({
|
||||
signatureImageAsBase64: true,
|
||||
typedSignature: true,
|
||||
}).nullish(),
|
||||
fields: ZFieldSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
}).array(),
|
||||
})
|
||||
.array(),
|
||||
|
||||
@ -129,7 +129,7 @@ export const setFieldsForTemplate = async ({
|
||||
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateNumberField(
|
||||
String(numberFieldParsedMeta.value || ''),
|
||||
String(numberFieldParsedMeta.value),
|
||||
numberFieldParsedMeta,
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
|
||||
@ -88,13 +88,11 @@ export const addUserToOrganisation = async ({
|
||||
organisationId,
|
||||
organisationGroups,
|
||||
organisationMemberRole,
|
||||
bypassEmail = false,
|
||||
}: {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
organisationGroups: OrganisationGroup[];
|
||||
organisationMemberRole: OrganisationMemberRole;
|
||||
bypassEmail?: boolean;
|
||||
}) => {
|
||||
const organisationGroupToUse = organisationGroups.find(
|
||||
(group) =>
|
||||
@ -124,15 +122,13 @@ export const addUserToOrganisation = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!bypassEmail) {
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-joined.email',
|
||||
payload: {
|
||||
organisationId,
|
||||
memberUserId: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-joined.email',
|
||||
payload: {
|
||||
organisationId,
|
||||
memberUserId: userId,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
347
packages/lib/server-only/pdf/auto-place-fields.ts
Normal file
@ -0,0 +1,347 @@
|
||||
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import PDFParser from 'pdf2json';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||
import { createEnvelopeFields } from '@documenso/lib/server-only/field/create-envelope-fields';
|
||||
import { type TFieldAndMeta, ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { EnvelopeIdOptions } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getPageSize } from './get-page-size';
|
||||
import {
|
||||
determineRecipientsForPlaceholders,
|
||||
extractRecipientPlaceholder,
|
||||
findRecipientByPlaceholder,
|
||||
parseFieldMetaFromPlaceholder,
|
||||
parseFieldTypeFromPlaceholder,
|
||||
} from './helpers';
|
||||
|
||||
const PLACEHOLDER_REGEX = /{{([^}]+)}}/g;
|
||||
const DEFAULT_FIELD_HEIGHT_PERCENT = 2;
|
||||
const WIDTH_ADJUSTMENT_FACTOR = 0.1;
|
||||
const MIN_HEIGHT_THRESHOLD = 0.01;
|
||||
|
||||
type TextPosition = {
|
||||
text: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
};
|
||||
|
||||
type CharIndexMapping = {
|
||||
textPositionIndex: number;
|
||||
};
|
||||
|
||||
type PlaceholderInfo = {
|
||||
placeholder: string;
|
||||
recipient: string;
|
||||
fieldAndMeta: TFieldAndMeta;
|
||||
page: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
};
|
||||
|
||||
type FieldToCreate = TFieldAndMeta & {
|
||||
envelopeItemId?: string;
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<PlaceholderInfo[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parser = new PDFParser(null, true);
|
||||
|
||||
parser.on('pdfParser_dataError', (errData) => {
|
||||
reject(errData);
|
||||
});
|
||||
|
||||
parser.on('pdfParser_dataReady', (pdfData) => {
|
||||
const placeholders: PlaceholderInfo[] = [];
|
||||
|
||||
pdfData.Pages.forEach((page, pageIndex) => {
|
||||
/*
|
||||
pdf2json returns the PDF page content as an array of characters.
|
||||
We need to concatenate the characters to get the full text.
|
||||
We also need to get the position of the text so we can place the placeholders in the correct position.
|
||||
|
||||
Page dimensions from PDF2JSON are in "page units" (relative coordinates)
|
||||
*/
|
||||
let pageText = '';
|
||||
const textPositions: TextPosition[] = [];
|
||||
const charIndexMappings: CharIndexMapping[] = [];
|
||||
|
||||
page.Texts.forEach((text) => {
|
||||
/*
|
||||
R is an array of objects containing each character, its position and styling information.
|
||||
The decodedText stores the characters, without any other information.
|
||||
|
||||
textPositions stores each character and its position on the page.
|
||||
*/
|
||||
const decodedText = text.R.map((run) => decodeURIComponent(run.T)).join('');
|
||||
|
||||
/*
|
||||
For each character in the decodedText, we store its position in the textPositions array.
|
||||
This allows us to quickly find the position of a character in the textPositions array by its index.
|
||||
*/
|
||||
for (let i = 0; i < decodedText.length; i++) {
|
||||
charIndexMappings.push({
|
||||
textPositionIndex: textPositions.length,
|
||||
});
|
||||
}
|
||||
|
||||
pageText += decodedText;
|
||||
|
||||
textPositions.push({
|
||||
text: decodedText,
|
||||
x: text.x,
|
||||
y: text.y,
|
||||
w: text.w || 0,
|
||||
});
|
||||
});
|
||||
|
||||
const placeholderMatches = pageText.matchAll(PLACEHOLDER_REGEX);
|
||||
|
||||
/*
|
||||
A placeholder match has the following format:
|
||||
|
||||
[
|
||||
'{{fieldType,recipient,fieldMeta}}',
|
||||
'fieldType,recipient,fieldMeta',
|
||||
'index: <number>',
|
||||
'input: <pdf-text>'
|
||||
]
|
||||
*/
|
||||
for (const placeholderMatch of placeholderMatches) {
|
||||
const placeholder = placeholderMatch[0];
|
||||
const placeholderData = placeholderMatch[1].split(',').map((property) => property.trim());
|
||||
|
||||
const [fieldTypeString, recipient, ...fieldMetaData] = placeholderData;
|
||||
|
||||
const rawFieldMeta = Object.fromEntries(
|
||||
fieldMetaData.map((property) => property.split('=')),
|
||||
);
|
||||
|
||||
const fieldType = parseFieldTypeFromPlaceholder(fieldTypeString);
|
||||
const parsedFieldMeta = parseFieldMetaFromPlaceholder(rawFieldMeta, fieldType);
|
||||
|
||||
const fieldAndMeta: TFieldAndMeta = ZFieldAndMetaSchema.parse({
|
||||
type: fieldType,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
});
|
||||
|
||||
/*
|
||||
Find the position of where the placeholder starts and ends in the text.
|
||||
|
||||
Then find the position of the characters in the textPositions array.
|
||||
This allows us to quickly find the position of a character in the textPositions array by its index.
|
||||
*/
|
||||
const placeholderEndCharIndex = placeholderMatch.index + placeholder.length;
|
||||
|
||||
/*
|
||||
Get the index of the placeholder's first and last character in the textPositions array.
|
||||
Used to retrieve the character information from the textPositions array.
|
||||
|
||||
Example:
|
||||
startTextPosIndex - 1
|
||||
endTextPosIndex - 40
|
||||
*/
|
||||
const startTextPosIndex = charIndexMappings[placeholderMatch.index].textPositionIndex;
|
||||
const endTextPosIndex = charIndexMappings[placeholderEndCharIndex - 1].textPositionIndex;
|
||||
|
||||
/*
|
||||
Get the placeholder's first and last character information from the textPositions array.
|
||||
|
||||
Example:
|
||||
placeholderStart = { text: '{', x: 100, y: 100, w: 100 }
|
||||
placeholderEnd = { text: '}', x: 200, y: 100, w: 100 }
|
||||
*/
|
||||
const placeholderStart = textPositions[startTextPosIndex];
|
||||
const placeholderEnd = textPositions[endTextPosIndex];
|
||||
|
||||
const width =
|
||||
placeholderEnd.x + placeholderEnd.w * WIDTH_ADJUSTMENT_FACTOR - placeholderStart.x;
|
||||
|
||||
placeholders.push({
|
||||
placeholder,
|
||||
recipient,
|
||||
fieldAndMeta,
|
||||
page: pageIndex + 1,
|
||||
x: placeholderStart.x,
|
||||
y: placeholderStart.y,
|
||||
width,
|
||||
height: 1,
|
||||
pageWidth: page.Width,
|
||||
pageHeight: page.Height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve(placeholders);
|
||||
});
|
||||
|
||||
parser.parseBuffer(pdf);
|
||||
});
|
||||
};
|
||||
|
||||
export const removePlaceholdersFromPDF = async (pdf: Buffer): Promise<Buffer> => {
|
||||
const placeholders = await extractPlaceholdersFromPDF(pdf);
|
||||
|
||||
const pdfDoc = await PDFDocument.load(new Uint8Array(pdf));
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
for (const placeholder of placeholders) {
|
||||
const pageIndex = placeholder.page - 1;
|
||||
const page = pages[pageIndex];
|
||||
|
||||
const { width: pdfLibPageWidth, height: pdfLibPageHeight } = getPageSize(page);
|
||||
|
||||
/*
|
||||
Convert PDF2JSON coordinates to pdf-lib coordinates:
|
||||
|
||||
PDF2JSON uses relative "page units":
|
||||
- x, y, width, height are in page units
|
||||
- Page dimensions (Width, Height) are also in page units
|
||||
|
||||
pdf-lib uses absolute points (1 point = 1/72 inch):
|
||||
- Need to convert from page units to points
|
||||
- Y-axis in pdf-lib is bottom-up (origin at bottom-left)
|
||||
- Y-axis in PDF2JSON is top-down (origin at top-left)
|
||||
*/
|
||||
|
||||
const xPoints = (placeholder.x / placeholder.pageWidth) * pdfLibPageWidth;
|
||||
const yPoints = pdfLibPageHeight - (placeholder.y / placeholder.pageHeight) * pdfLibPageHeight;
|
||||
const widthPoints = (placeholder.width / placeholder.pageWidth) * pdfLibPageWidth;
|
||||
const heightPoints = (placeholder.height / placeholder.pageHeight) * pdfLibPageHeight;
|
||||
|
||||
page.drawRectangle({
|
||||
x: xPoints,
|
||||
y: yPoints - heightPoints, // Adjust for height since y is at baseline
|
||||
width: widthPoints,
|
||||
height: heightPoints,
|
||||
color: rgb(1, 1, 1),
|
||||
borderColor: rgb(1, 1, 1),
|
||||
borderWidth: 2,
|
||||
});
|
||||
}
|
||||
|
||||
const modifiedPdfBytes = await pdfDoc.save();
|
||||
|
||||
return Buffer.from(modifiedPdfBytes);
|
||||
};
|
||||
|
||||
export const insertFieldsFromPlaceholdersInPDF = async (
|
||||
pdf: Buffer,
|
||||
userId: number,
|
||||
teamId: number,
|
||||
envelopeId: EnvelopeIdOptions,
|
||||
requestMetadata: ApiRequestMetadata,
|
||||
envelopeItemId?: string,
|
||||
recipients?: Pick<Recipient, 'id' | 'email'>[],
|
||||
): Promise<Buffer> => {
|
||||
const placeholders = await extractPlaceholdersFromPDF(pdf);
|
||||
|
||||
if (placeholders.length === 0) {
|
||||
return pdf;
|
||||
}
|
||||
|
||||
/*
|
||||
A structure that maps the recipient index to the recipient name.
|
||||
Example: 1 => 'Recipient 1'
|
||||
*/
|
||||
const recipientPlaceholders = new Map<number, string>();
|
||||
|
||||
for (const placeholder of placeholders) {
|
||||
const { name, recipientIndex } = extractRecipientPlaceholder(placeholder.recipient);
|
||||
|
||||
recipientPlaceholders.set(recipientIndex, name);
|
||||
}
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: envelopeId,
|
||||
userId,
|
||||
teamId,
|
||||
type: null,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
secondaryId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
const createdRecipients = await determineRecipientsForPlaceholders(
|
||||
recipients,
|
||||
recipientPlaceholders,
|
||||
envelope,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
);
|
||||
|
||||
const fieldsToCreate: FieldToCreate[] = [];
|
||||
|
||||
for (const placeholder of placeholders) {
|
||||
/*
|
||||
Convert PDF2JSON coordinates to percentage-based coordinates (0-100)
|
||||
The UI expects positionX and positionY as percentages, not absolute points
|
||||
PDF2JSON uses relative coordinates: x/pageWidth and y/pageHeight give us the percentage
|
||||
*/
|
||||
const xPercent = (placeholder.x / placeholder.pageWidth) * 100;
|
||||
const yPercent = (placeholder.y / placeholder.pageHeight) * 100;
|
||||
|
||||
const widthPercent = (placeholder.width / placeholder.pageWidth) * 100;
|
||||
const heightPercent = (placeholder.height / placeholder.pageHeight) * 100;
|
||||
|
||||
const recipient = findRecipientByPlaceholder(
|
||||
placeholder.recipient,
|
||||
placeholder.placeholder,
|
||||
recipients,
|
||||
createdRecipients,
|
||||
);
|
||||
|
||||
// Default height percentage if too small (use 2% as a reasonable default)
|
||||
const finalHeightPercent =
|
||||
heightPercent > MIN_HEIGHT_THRESHOLD ? heightPercent : DEFAULT_FIELD_HEIGHT_PERCENT;
|
||||
|
||||
fieldsToCreate.push({
|
||||
...placeholder.fieldAndMeta,
|
||||
envelopeItemId,
|
||||
recipientId: recipient.id,
|
||||
pageNumber: placeholder.page,
|
||||
pageX: xPercent,
|
||||
pageY: yPercent,
|
||||
width: widthPercent,
|
||||
height: finalHeightPercent,
|
||||
});
|
||||
}
|
||||
|
||||
await createEnvelopeFields({
|
||||
userId,
|
||||
teamId,
|
||||
id: envelopeId,
|
||||
fields: fieldsToCreate,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
return pdf;
|
||||
};
|
||||
261
packages/lib/server-only/pdf/helpers.ts
Normal file
@ -0,0 +1,261 @@
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { type Envelope, EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createDocumentRecipients } from '@documenso/lib/server-only/recipient/create-document-recipients';
|
||||
import { createTemplateRecipients } from '@documenso/lib/server-only/recipient/create-template-recipients';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||
import type { EnvelopeIdOptions } from '@documenso/lib/utils/envelope';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
type RecipientPlaceholderInfo = {
|
||||
email: string;
|
||||
name: string;
|
||||
recipientIndex: number;
|
||||
};
|
||||
|
||||
/*
|
||||
Parse field type string to FieldType enum.
|
||||
Normalizes the input (uppercase, trim) and validates it's a valid field type.
|
||||
This ensures we handle case variations and whitespace, and provides clear error messages.
|
||||
*/
|
||||
export const parseFieldTypeFromPlaceholder = (fieldTypeString: string): FieldType => {
|
||||
const normalizedType = fieldTypeString.toUpperCase().trim();
|
||||
|
||||
return match(normalizedType)
|
||||
.with('SIGNATURE', () => FieldType.SIGNATURE)
|
||||
.with('FREE_SIGNATURE', () => FieldType.FREE_SIGNATURE)
|
||||
.with('INITIALS', () => FieldType.INITIALS)
|
||||
.with('NAME', () => FieldType.NAME)
|
||||
.with('EMAIL', () => FieldType.EMAIL)
|
||||
.with('DATE', () => FieldType.DATE)
|
||||
.with('TEXT', () => FieldType.TEXT)
|
||||
.with('NUMBER', () => FieldType.NUMBER)
|
||||
.with('RADIO', () => FieldType.RADIO)
|
||||
.with('CHECKBOX', () => FieldType.CHECKBOX)
|
||||
.with('DROPDOWN', () => FieldType.DROPDOWN)
|
||||
.otherwise(() => {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Invalid field type: ${fieldTypeString}`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Transform raw field metadata from placeholder format to schema format.
|
||||
Users should provide properly capitalized property names (e.g., readOnly, fontSize, textAlign).
|
||||
Converts string values to proper types (booleans, numbers).
|
||||
*/
|
||||
export const parseFieldMetaFromPlaceholder = (
|
||||
rawFieldMeta: Record<string, string>,
|
||||
fieldType: FieldType,
|
||||
): Record<string, unknown> | undefined => {
|
||||
if (fieldType === FieldType.SIGNATURE || fieldType === FieldType.FREE_SIGNATURE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(rawFieldMeta).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldTypeString = String(fieldType).toLowerCase();
|
||||
|
||||
const parsedFieldMeta: Record<string, boolean | number | string> = {
|
||||
type: fieldTypeString,
|
||||
};
|
||||
|
||||
/*
|
||||
rawFieldMeta is an object with string keys and string values.
|
||||
It contains string values because the PDF parser returns the values as strings.
|
||||
|
||||
E.g. { 'required': 'true', 'fontSize': '12', 'maxValue': '100', 'minValue': '0', 'characterLimit': '100' }
|
||||
*/
|
||||
const rawFieldMetaEntries = Object.entries(rawFieldMeta);
|
||||
|
||||
for (const [property, value] of rawFieldMetaEntries) {
|
||||
if (property === 'readOnly' || property === 'required') {
|
||||
parsedFieldMeta[property] = value === 'true';
|
||||
} else if (
|
||||
property === 'fontSize' ||
|
||||
property === 'maxValue' ||
|
||||
property === 'minValue' ||
|
||||
property === 'characterLimit'
|
||||
) {
|
||||
const numValue = Number(value);
|
||||
|
||||
if (!Number.isNaN(numValue)) {
|
||||
parsedFieldMeta[property] = numValue;
|
||||
}
|
||||
} else {
|
||||
parsedFieldMeta[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return parsedFieldMeta;
|
||||
};
|
||||
|
||||
export const extractRecipientPlaceholder = (placeholder: string): RecipientPlaceholderInfo => {
|
||||
const indexMatch = placeholder.match(/^r(\d+)$/i);
|
||||
|
||||
if (!indexMatch) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Invalid recipient placeholder format: ${placeholder}. Expected format: r1, r2, r3, etc.`,
|
||||
});
|
||||
}
|
||||
|
||||
const recipientIndex = Number(indexMatch[1]);
|
||||
|
||||
return {
|
||||
email: `recipient.${recipientIndex}@documenso.com`,
|
||||
name: `Recipient ${recipientIndex}`,
|
||||
recipientIndex,
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
Finds a recipient based on a placeholder reference.
|
||||
If recipients array is provided, uses index-based matching (r1 -> recipients[0], etc.).
|
||||
Otherwise, uses email-based matching from createdRecipients.
|
||||
*/
|
||||
export const findRecipientByPlaceholder = (
|
||||
recipientPlaceholder: string,
|
||||
placeholder: string,
|
||||
recipients: Pick<Recipient, 'id' | 'email'>[] | undefined,
|
||||
createdRecipients: Pick<Recipient, 'id' | 'email'>[],
|
||||
): Pick<Recipient, 'id' | 'email'> => {
|
||||
if (recipients && recipients.length > 0) {
|
||||
/*
|
||||
Map placeholder by index: r1 -> recipients[0], r2 -> recipients[1], etc.
|
||||
recipientIndex is 1-based, so we subtract 1 to get the array index.
|
||||
*/
|
||||
const { recipientIndex } = extractRecipientPlaceholder(recipientPlaceholder);
|
||||
const recipientArrayIndex = recipientIndex - 1;
|
||||
|
||||
if (recipientArrayIndex < 0 || recipientArrayIndex >= recipients.length) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Recipient placeholder ${recipientPlaceholder} (index ${recipientIndex}) is out of range. Provided ${recipients.length} recipient(s).`,
|
||||
});
|
||||
}
|
||||
|
||||
return recipients[recipientArrayIndex];
|
||||
}
|
||||
|
||||
/*
|
||||
Use email-based matching for placeholder recipients.
|
||||
*/
|
||||
const { email } = extractRecipientPlaceholder(recipientPlaceholder);
|
||||
const recipient = createdRecipients.find((r) => r.email === email);
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Could not find recipient ID for placeholder: ${placeholder}`,
|
||||
});
|
||||
}
|
||||
|
||||
return recipient;
|
||||
};
|
||||
|
||||
/*
|
||||
Determines the recipients to use for field creation.
|
||||
If recipients are provided, uses them directly.
|
||||
Otherwise, creates recipients from placeholders.
|
||||
*/
|
||||
export const determineRecipientsForPlaceholders = async (
|
||||
recipients: Pick<Recipient, 'id' | 'email'>[] | undefined,
|
||||
recipientPlaceholders: Map<number, string>,
|
||||
envelope: Pick<Envelope, 'id' | 'type' | 'secondaryId'>,
|
||||
userId: number,
|
||||
teamId: number,
|
||||
requestMetadata: ApiRequestMetadata,
|
||||
): Promise<Pick<Recipient, 'id' | 'email'>[]> => {
|
||||
if (recipients && recipients.length > 0) {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
return createRecipientsFromPlaceholders(
|
||||
recipientPlaceholders,
|
||||
envelope,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
);
|
||||
};
|
||||
|
||||
export const createRecipientsFromPlaceholders = async (
|
||||
recipientPlaceholders: Map<number, string>,
|
||||
envelope: Pick<Envelope, 'id' | 'type' | 'secondaryId'>,
|
||||
userId: number,
|
||||
teamId: number,
|
||||
requestMetadata: ApiRequestMetadata,
|
||||
): Promise<Pick<Recipient, 'id' | 'email'>[]> => {
|
||||
const recipientsToCreate = Array.from(
|
||||
recipientPlaceholders.entries(),
|
||||
([recipientIndex, name]) => {
|
||||
return {
|
||||
email: `recipient.${recipientIndex}@documenso.com`,
|
||||
name,
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: recipientIndex,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const existingRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const existingEmails = new Set(existingRecipients.map((r) => r.email));
|
||||
const recipientsToCreateFiltered = recipientsToCreate.filter(
|
||||
(recipient) => !existingEmails.has(recipient.email),
|
||||
);
|
||||
|
||||
if (recipientsToCreateFiltered.length === 0) {
|
||||
return existingRecipients;
|
||||
}
|
||||
|
||||
const newRecipients = await match(envelope.type)
|
||||
.with(EnvelopeType.DOCUMENT, async () => {
|
||||
const envelopeId: EnvelopeIdOptions = {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
};
|
||||
|
||||
const { recipients } = await createDocumentRecipients({
|
||||
userId,
|
||||
teamId,
|
||||
id: envelopeId,
|
||||
recipients: recipientsToCreateFiltered,
|
||||
requestMetadata,
|
||||
});
|
||||
|
||||
return recipients;
|
||||
})
|
||||
.with(EnvelopeType.TEMPLATE, async () => {
|
||||
const templateId = mapSecondaryIdToTemplateId(envelope.secondaryId ?? '');
|
||||
|
||||
const { recipients } = await createTemplateRecipients({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
recipients: recipientsToCreateFiltered,
|
||||
});
|
||||
|
||||
return recipients;
|
||||
})
|
||||
.otherwise(() => {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Invalid envelope type: ${envelope.type}`,
|
||||
});
|
||||
});
|
||||
|
||||
return [...existingRecipients, ...newRecipients];
|
||||
};
|
||||
@ -32,8 +32,10 @@ export const insertFieldInPDFV2 = async ({
|
||||
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
|
||||
const layer = new Konva.Layer();
|
||||
|
||||
const insertedFields = fields.filter((field) => field.inserted);
|
||||
|
||||
// Render the fields onto the layer.
|
||||
for (const field of fields) {
|
||||
for (const field of insertedFields) {
|
||||
renderField({
|
||||
scale: 1,
|
||||
field: {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { PDFDocument } from '@cantoo/pdf-lib';
|
||||
|
||||
import { removePlaceholdersFromPDF } from './auto-place-fields';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { flattenAnnotations } from './flatten-annotations';
|
||||
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
|
||||
@ -22,6 +23,7 @@ export const normalizePdf = async (pdf: Buffer) => {
|
||||
removeOptionalContentGroups(pdfDoc);
|
||||
await flattenForm(pdfDoc);
|
||||
flattenAnnotations(pdfDoc);
|
||||
const pdfWithoutPlaceholders = await removePlaceholdersFromPDF(pdf);
|
||||
|
||||
return Buffer.from(await pdfDoc.save());
|
||||
return pdfWithoutPlaceholders;
|
||||
};
|
||||
|
||||
@ -215,12 +215,6 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
|
||||
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
|
||||
|
||||
// Custom logic for V2 to include all fields, since v1 excludes read only
|
||||
// and prefilled fields.
|
||||
if (directTemplateEnvelope.internalVersion === 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Include if it's required or has a signed value
|
||||
return isRequiredField(templateField) || signedFieldValue !== undefined;
|
||||
});
|
||||
@ -474,28 +468,19 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
signingOrder: directTemplateRecipient.signingOrder,
|
||||
fields: {
|
||||
createMany: {
|
||||
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => {
|
||||
let inserted = true;
|
||||
|
||||
// Custom logic for V2 to only insert if values exist.
|
||||
if (directTemplateEnvelope.internalVersion === 2) {
|
||||
inserted = customText !== '';
|
||||
}
|
||||
|
||||
return {
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
positionX: templateField.positionX,
|
||||
positionY: templateField.positionY,
|
||||
width: templateField.width,
|
||||
height: templateField.height,
|
||||
customText: customText ?? '',
|
||||
inserted,
|
||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||
};
|
||||
}),
|
||||
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
|
||||
envelopeId: createdEnvelope.id,
|
||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||
type: templateField.type,
|
||||
page: templateField.page,
|
||||
positionX: templateField.positionX,
|
||||
positionY: templateField.positionY,
|
||||
width: templateField.width,
|
||||
height: templateField.height,
|
||||
customText: customText ?? '',
|
||||
inserted: true,
|
||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,46 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '../constants/pdf';
|
||||
|
||||
export const FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN = 'middle';
|
||||
export const FIELD_DEFAULT_GENERIC_ALIGN = 'left';
|
||||
export const FIELD_DEFAULT_LINE_HEIGHT = 1;
|
||||
export const FIELD_DEFAULT_LETTER_SPACING = 0;
|
||||
|
||||
export const FIELD_MIN_LINE_HEIGHT = 1;
|
||||
export const FIELD_MAX_LINE_HEIGHT = 10;
|
||||
|
||||
export const FIELD_MIN_LETTER_SPACING = 0;
|
||||
export const FIELD_MAX_LETTER_SPACING = 100;
|
||||
|
||||
export const DEFAULT_FIELD_FONT_SIZE = 12;
|
||||
|
||||
/**
|
||||
* Grouped field types that use the same generic text rendering function.
|
||||
*/
|
||||
export type GenericTextFieldTypeMetas =
|
||||
| TInitialsFieldMeta
|
||||
| TNameFieldMeta
|
||||
| TEmailFieldMeta
|
||||
| TDateFieldMeta
|
||||
| TTextFieldMeta
|
||||
| TNumberFieldMeta;
|
||||
|
||||
const ZFieldMetaLineHeight = z.coerce
|
||||
.number()
|
||||
.min(FIELD_MIN_LINE_HEIGHT)
|
||||
.max(FIELD_MAX_LINE_HEIGHT)
|
||||
.describe('The line height of the text');
|
||||
const ZFieldMetaLetterSpacing = z.coerce
|
||||
.number()
|
||||
.min(FIELD_MIN_LETTER_SPACING)
|
||||
.max(FIELD_MAX_LETTER_SPACING)
|
||||
.describe('The spacing between each character');
|
||||
const ZFieldMetaVerticalAlign = z
|
||||
.enum(['top', 'middle', 'bottom'])
|
||||
.describe('The vertical alignment of the text');
|
||||
export const DEFAULT_FIELD_FONT_SIZE = 14;
|
||||
|
||||
export const ZBaseFieldMeta = z.object({
|
||||
label: z.string().optional(),
|
||||
@ -87,14 +50,8 @@ export type TDateFieldMeta = z.infer<typeof ZDateFieldMeta>;
|
||||
export const ZTextFieldMeta = ZBaseFieldMeta.extend({
|
||||
type: z.literal('text'),
|
||||
text: z.string().optional(),
|
||||
characterLimit: z.coerce
|
||||
.number({ invalid_type_error: msg`Value must be a number`.id })
|
||||
.min(0)
|
||||
.optional(),
|
||||
characterLimit: z.number().optional(),
|
||||
textAlign: ZFieldTextAlignSchema.optional(),
|
||||
lineHeight: ZFieldMetaLineHeight.nullish(),
|
||||
letterSpacing: ZFieldMetaLetterSpacing.nullish(),
|
||||
verticalAlign: ZFieldMetaVerticalAlign.nullish(),
|
||||
});
|
||||
|
||||
export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
|
||||
@ -106,9 +63,6 @@ export const ZNumberFieldMeta = ZBaseFieldMeta.extend({
|
||||
minValue: z.coerce.number().nullish(),
|
||||
maxValue: z.coerce.number().nullish(),
|
||||
textAlign: ZFieldTextAlignSchema.optional(),
|
||||
lineHeight: ZFieldMetaLineHeight.nullish(),
|
||||
letterSpacing: ZFieldMetaLetterSpacing.nullish(),
|
||||
verticalAlign: ZFieldMetaVerticalAlign.nullish(),
|
||||
});
|
||||
|
||||
export type TNumberFieldMeta = z.infer<typeof ZNumberFieldMeta>;
|
||||
|
||||
@ -26,7 +26,7 @@ export const renderCheckboxFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { pageWidth, pageHeight, pageLayer, mode, color } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
@ -210,9 +210,7 @@ export const renderCheckboxFieldElement = (
|
||||
fieldGroup.add(text);
|
||||
});
|
||||
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -50,7 +50,7 @@ export const renderDropdownFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { pageWidth, pageHeight, pageLayer, mode, translations, color } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode, translations } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
@ -74,21 +74,6 @@ export const renderDropdownFieldElement = (
|
||||
|
||||
const fontSize = dropdownMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
// Don't show any labels when exporting.
|
||||
if (mode === 'export') {
|
||||
selectedValue = '';
|
||||
}
|
||||
|
||||
// Render the default value if readonly.
|
||||
if (
|
||||
dropdownMeta?.readOnly &&
|
||||
dropdownMeta.defaultValue &&
|
||||
dropdownMeta.values &&
|
||||
dropdownMeta.values.some((value) => value.value === dropdownMeta.defaultValue)
|
||||
) {
|
||||
selectedValue = dropdownMeta.defaultValue;
|
||||
}
|
||||
|
||||
if (field.inserted) {
|
||||
selectedValue = field.customText;
|
||||
}
|
||||
@ -181,9 +166,7 @@ export const renderDropdownFieldElement = (
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -77,7 +77,6 @@ export const renderField = ({
|
||||
scale,
|
||||
};
|
||||
|
||||
// If the generic text field element array changes, update the `GenericTextFieldTypeMetas` type
|
||||
return match(field.type)
|
||||
.with(
|
||||
FieldType.INITIALS,
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
import Konva from 'konva';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { GenericTextFieldTypeMetas } from '../../types/field-meta';
|
||||
import {
|
||||
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||
FIELD_DEFAULT_LETTER_SPACING,
|
||||
FIELD_DEFAULT_LINE_HEIGHT,
|
||||
} from '../../types/field-meta';
|
||||
import type { TTextFieldMeta } from '../../types/field-meta';
|
||||
import {
|
||||
createFieldHoverInteraction,
|
||||
konvaTextFill,
|
||||
@ -18,14 +12,14 @@ import {
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
|
||||
const DEFAULT_TEXT_X_PADDING = 6;
|
||||
const DEFAULT_TEXT_ALIGN = 'left';
|
||||
|
||||
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
const fieldMeta = field.fieldMeta as GenericTextFieldTypeMetas | undefined;
|
||||
const textMeta = field.fieldMeta as TTextFieldMeta | undefined;
|
||||
|
||||
const fieldTypeName = translations?.[field.type] || field.type;
|
||||
|
||||
@ -39,77 +33,53 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
// Calculate text positioning based on alignment
|
||||
const textX = 0;
|
||||
const textY = 0;
|
||||
const textFontSize = fieldMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN;
|
||||
const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle';
|
||||
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
const textPadding = 10;
|
||||
|
||||
// By default, render the field name or label centered
|
||||
let textToRender: string = fieldMeta?.label || fieldTypeName;
|
||||
let textAlign: 'left' | 'center' | 'right' = 'center';
|
||||
let textVerticalAlign: 'top' | 'middle' | 'bottom' = FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
let textLineHeight = FIELD_DEFAULT_LINE_HEIGHT;
|
||||
let textLetterSpacing = FIELD_DEFAULT_LETTER_SPACING;
|
||||
let textToRender: string = fieldTypeName;
|
||||
|
||||
// Render default values for text/number if provided for editing mode.
|
||||
if (mode === 'edit' && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) {
|
||||
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value;
|
||||
|
||||
if (value) {
|
||||
textToRender = value;
|
||||
|
||||
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
textAlign = fieldMeta.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
|
||||
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
|
||||
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
|
||||
// Handle edit mode.
|
||||
if (mode === 'edit') {
|
||||
if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
} else {
|
||||
// Show field name which is centered for the edit mode if no label/text is avaliable.
|
||||
textToRender = textMeta?.label || fieldTypeName;
|
||||
textAlign = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
// Default to blank for export mode since we want to ensure we don't show
|
||||
// any placeholder text or labels unless actually it's inserted.
|
||||
if (mode === 'export') {
|
||||
textToRender = '';
|
||||
}
|
||||
// Handle sign mode.
|
||||
if (mode === 'sign' || mode === 'export') {
|
||||
if (!field.inserted) {
|
||||
if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
} else if (mode === 'sign') {
|
||||
// Only show the field name in sign mode if no text/label is avaliable.
|
||||
textToRender = textMeta?.label || fieldTypeName;
|
||||
textAlign = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback render readonly fields if prefilled value exists.
|
||||
if (field?.fieldMeta?.readOnly && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) {
|
||||
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value;
|
||||
|
||||
if (value) {
|
||||
textToRender = value;
|
||||
|
||||
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
textAlign = fieldMeta.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
|
||||
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
|
||||
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
|
||||
if (field.inserted) {
|
||||
textToRender = field.customText;
|
||||
}
|
||||
}
|
||||
|
||||
// Override everything with value if it's inserted.
|
||||
if (field.inserted) {
|
||||
textToRender = field.customText;
|
||||
|
||||
textAlign = fieldMeta?.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
|
||||
|
||||
if (fieldMeta?.type === 'text' || fieldMeta?.type === 'number') {
|
||||
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
|
||||
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Do not use native text padding since it's uniform.
|
||||
// We only want to have padding on the left and right hand sides.
|
||||
fieldText.setAttrs({
|
||||
x: textX + DEFAULT_TEXT_X_PADDING,
|
||||
x: textX,
|
||||
y: textY,
|
||||
verticalAlign: textVerticalAlign,
|
||||
wrap: 'word',
|
||||
padding: textPadding,
|
||||
text: textToRender,
|
||||
fontSize: textFontSize,
|
||||
align: textAlign,
|
||||
lineHeight: textLineHeight,
|
||||
letterSpacing: textLetterSpacing,
|
||||
fontFamily: konvaTextFontFamily,
|
||||
fill: konvaTextFill,
|
||||
width: fieldWidth - DEFAULT_TEXT_X_PADDING * 2,
|
||||
align: textAlign,
|
||||
width: fieldWidth,
|
||||
height: fieldHeight,
|
||||
} satisfies Partial<Konva.TextConfig>);
|
||||
|
||||
@ -120,7 +90,7 @@ export const renderGenericTextFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { mode = 'edit', pageLayer, color } = options;
|
||||
const { mode = 'edit', pageLayer } = options;
|
||||
|
||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||
|
||||
@ -155,7 +125,7 @@ export const renderGenericTextFieldElement = (
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2);
|
||||
fieldText.width(rectWidth);
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
@ -173,7 +143,7 @@ export const renderGenericTextFieldElement = (
|
||||
const rectHeight = fieldRect.height();
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2);
|
||||
fieldText.width(rectWidth); // Account for padding
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
@ -188,9 +158,7 @@ export const renderGenericTextFieldElement = (
|
||||
fieldRect.opacity(0);
|
||||
}
|
||||
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -25,7 +25,7 @@ export const renderRadioFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { pageWidth, pageHeight, pageLayer, mode, color } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode } = options;
|
||||
|
||||
const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
|
||||
const radioValues = radioMeta?.values || [];
|
||||
@ -195,9 +195,7 @@ export const renderRadioFieldElement = (
|
||||
fieldGroup.add(text);
|
||||
});
|
||||
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -142,7 +142,7 @@ export const renderSignatureFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { mode = 'edit', pageLayer, color } = options;
|
||||
const { mode = 'edit', pageLayer } = options;
|
||||
|
||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||
|
||||
@ -211,9 +211,7 @@ export const renderSignatureFieldElement = (
|
||||
fieldRect.opacity(0);
|
||||
}
|
||||
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -102,7 +102,7 @@ export const extractFieldInsertionValues = ({
|
||||
}
|
||||
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateNumberField(fieldValue.value, numberFieldParsedMeta, true);
|
||||
const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
@ -111,7 +111,7 @@ export const extractFieldInsertionValues = ({
|
||||
}
|
||||
|
||||
return {
|
||||
customText: fieldValue.value,
|
||||
customText: fieldValue.value.toString(),
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
import type { Envelope, Recipient } from '@prisma/client';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -162,9 +156,8 @@ export const canEnvelopeItemsBeModified = (
|
||||
if (
|
||||
recipients.some(
|
||||
(recipient) =>
|
||||
recipient.role !== RecipientRole.CC &&
|
||||
(recipient.signingStatus === SigningStatus.SIGNED ||
|
||||
recipient.sendStatus === SendStatus.SENT),
|
||||
recipient.signingStatus === SigningStatus.SIGNED ||
|
||||
recipient.sendStatus === SendStatus.SENT,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
|
||||
@ -1,20 +1,13 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { ALIGNMENT_TEST_FIELDS } from '@documenso/app-tests/constants/field-alignment-pdf';
|
||||
import { formatAlignmentTestFields } from '@documenso/app-tests/constants/field-alignment-pdf';
|
||||
import { FIELD_META_TEST_FIELDS } from '@documenso/app-tests/constants/field-meta-pdf';
|
||||
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||
import {
|
||||
incrementDocumentId,
|
||||
incrementTemplateId,
|
||||
} from '@documenso/lib/server-only/envelope/increment-id';
|
||||
import { incrementDocumentId } from '@documenso/lib/server-only/envelope/increment-id';
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
|
||||
import { prisma } from '..';
|
||||
import {
|
||||
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
DIRECT_TEMPLATE_RECIPIENT_NAME,
|
||||
} from '../../lib/constants/direct-templates';
|
||||
import {
|
||||
DocumentDataType,
|
||||
DocumentSource,
|
||||
@ -183,26 +176,6 @@ export const seedDatabase = async () => {
|
||||
userId: adminUser.user.id,
|
||||
teamId: adminUser.team.id,
|
||||
}),
|
||||
seedAlignmentTestDocument({
|
||||
userId: exampleUser.user.id,
|
||||
teamId: exampleUser.team.id,
|
||||
recipientName: exampleUser.user.name || '',
|
||||
recipientEmail: exampleUser.user.email,
|
||||
insertFields: false,
|
||||
status: DocumentStatus.DRAFT,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
}),
|
||||
seedAlignmentTestDocument({
|
||||
userId: exampleUser.user.id,
|
||||
teamId: exampleUser.team.id,
|
||||
recipientName: exampleUser.user.name || '',
|
||||
recipientEmail: exampleUser.user.email,
|
||||
insertFields: false,
|
||||
status: DocumentStatus.DRAFT,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
isDirectTemplate: true,
|
||||
directTemplateToken: 'example',
|
||||
}),
|
||||
seedAlignmentTestDocument({
|
||||
userId: exampleUser.user.id,
|
||||
teamId: exampleUser.team.id,
|
||||
@ -219,26 +192,6 @@ export const seedDatabase = async () => {
|
||||
insertFields: true,
|
||||
status: DocumentStatus.PENDING,
|
||||
}),
|
||||
seedAlignmentTestDocument({
|
||||
userId: adminUser.user.id,
|
||||
teamId: adminUser.team.id,
|
||||
recipientName: adminUser.user.name || '',
|
||||
recipientEmail: adminUser.user.email,
|
||||
insertFields: false,
|
||||
status: DocumentStatus.DRAFT,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
}),
|
||||
seedAlignmentTestDocument({
|
||||
userId: adminUser.user.id,
|
||||
teamId: adminUser.team.id,
|
||||
recipientName: adminUser.user.name || '',
|
||||
recipientEmail: adminUser.user.email,
|
||||
insertFields: false,
|
||||
status: DocumentStatus.DRAFT,
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
isDirectTemplate: true,
|
||||
directTemplateToken: 'admin',
|
||||
}),
|
||||
seedAlignmentTestDocument({
|
||||
userId: adminUser.user.id,
|
||||
teamId: adminUser.team.id,
|
||||
@ -261,25 +214,17 @@ export const seedDatabase = async () => {
|
||||
export const seedAlignmentTestDocument = async ({
|
||||
userId,
|
||||
teamId,
|
||||
title = 'Envelope Full Field Test',
|
||||
recipientName,
|
||||
recipientEmail,
|
||||
insertFields,
|
||||
status,
|
||||
type = EnvelopeType.DOCUMENT,
|
||||
isDirectTemplate = false,
|
||||
directTemplateToken,
|
||||
}: {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
title?: string;
|
||||
recipientName: string;
|
||||
recipientEmail: string;
|
||||
insertFields: boolean;
|
||||
status: DocumentStatus;
|
||||
type?: EnvelopeType;
|
||||
isDirectTemplate?: boolean;
|
||||
directTemplateToken?: string;
|
||||
}) => {
|
||||
const alignmentPdf = fs
|
||||
.readFileSync(path.join(__dirname, '../../../assets/field-font-alignment.pdf'))
|
||||
@ -292,10 +237,7 @@ export const seedAlignmentTestDocument = async ({
|
||||
const alignmentDocumentData = await createDocumentData({ documentData: alignmentPdf });
|
||||
const fieldMetaDocumentData = await createDocumentData({ documentData: fieldMetaPdf });
|
||||
|
||||
const secondaryId =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
||||
const documentId = await incrementDocumentId();
|
||||
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: {},
|
||||
@ -304,12 +246,12 @@ export const seedAlignmentTestDocument = async ({
|
||||
const createdEnvelope = await prisma.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId,
|
||||
secondaryId: documentId.formattedDocumentId,
|
||||
internalVersion: 2,
|
||||
type,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
documentMetaId: documentMeta.id,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
title,
|
||||
title: `Envelope Full Field Test`,
|
||||
status,
|
||||
envelopeItems: {
|
||||
createMany: {
|
||||
@ -333,8 +275,8 @@ export const seedAlignmentTestDocument = async ({
|
||||
teamId,
|
||||
recipients: {
|
||||
create: {
|
||||
name: isDirectTemplate ? DIRECT_TEMPLATE_RECIPIENT_NAME : recipientName,
|
||||
email: isDirectTemplate ? DIRECT_TEMPLATE_RECIPIENT_EMAIL : recipientEmail,
|
||||
name: recipientName,
|
||||
email: recipientEmail,
|
||||
token: nanoid(),
|
||||
sendStatus: status === 'DRAFT' ? SendStatus.NOT_SENT : SendStatus.SENT,
|
||||
signingStatus: status === 'COMPLETED' ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
@ -350,25 +292,6 @@ export const seedAlignmentTestDocument = async ({
|
||||
|
||||
const { id, recipients, envelopeItems } = createdEnvelope;
|
||||
|
||||
if (isDirectTemplate) {
|
||||
const directTemplateRecpient = recipients.find(
|
||||
(recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
);
|
||||
|
||||
if (!directTemplateRecpient) {
|
||||
throw new Error('Need to create a direct template recipient');
|
||||
}
|
||||
|
||||
await prisma.templateDirectLink.create({
|
||||
data: {
|
||||
envelopeId: id,
|
||||
enabled: true,
|
||||
token: directTemplateToken ?? Math.random().toString(),
|
||||
directTemplateRecipientId: directTemplateRecpient.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const recipientId = recipients[0].id;
|
||||
const envelopeItemAlignmentItem = envelopeItems.find((item) => item.order === 1)?.id;
|
||||
const envelopeItemFieldMetaItem = envelopeItems.find((item) => item.order === 2)?.id;
|
||||
@ -378,7 +301,7 @@ export const seedAlignmentTestDocument = async ({
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
ALIGNMENT_TEST_FIELDS.map(async (field) => {
|
||||
formatAlignmentTestFields.map(async (field) => {
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
...field,
|
||||
@ -386,20 +309,16 @@ export const seedAlignmentTestDocument = async ({
|
||||
envelopeItemId: envelopeItemAlignmentItem,
|
||||
envelopeId: id,
|
||||
customText: insertFields ? field.customText : '',
|
||||
inserted:
|
||||
insertFields &&
|
||||
((!field?.fieldMeta?.readOnly && Boolean(field.customText)) ||
|
||||
field.type === 'SIGNATURE'),
|
||||
signature:
|
||||
field.signature && insertFields
|
||||
? {
|
||||
create: {
|
||||
recipientId,
|
||||
signatureImageAsBase64: isBase64Image(field.signature) ? field.signature : null,
|
||||
typedSignature: isBase64Image(field.signature) ? null : field.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
inserted: insertFields,
|
||||
signature: field.signature
|
||||
? {
|
||||
create: {
|
||||
recipientId,
|
||||
signatureImageAsBase64: isBase64Image(field.signature) ? field.signature : null,
|
||||
typedSignature: isBase64Image(field.signature) ? null : field.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}),
|
||||
@ -414,20 +333,16 @@ export const seedAlignmentTestDocument = async ({
|
||||
envelopeItemId: envelopeItemFieldMetaItem,
|
||||
envelopeId: id,
|
||||
customText: insertFields ? field.customText : '',
|
||||
inserted:
|
||||
insertFields &&
|
||||
((!field?.fieldMeta?.readOnly && Boolean(field.customText)) ||
|
||||
field.type === 'SIGNATURE'),
|
||||
signature:
|
||||
field.signature && insertFields
|
||||
? {
|
||||
create: {
|
||||
recipientId,
|
||||
signatureImageAsBase64: isBase64Image(field.signature) ? field.signature : null,
|
||||
typedSignature: isBase64Image(field.signature) ? null : field.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
inserted: insertFields,
|
||||
signature: field.signature
|
||||
? {
|
||||
create: {
|
||||
recipientId,
|
||||
signatureImageAsBase64: isBase64Image(field.signature) ? field.signature : null,
|
||||
typedSignature: isBase64Image(field.signature) ? null : field.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@ -64,7 +64,6 @@ export const seedOrganisationMembers = async ({
|
||||
organisationId,
|
||||
organisationGroups,
|
||||
organisationMemberRole: member.organisationRole,
|
||||
bypassEmail: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ export const ZSignEnvelopeFieldValue = z.discriminatedUnion('type', [
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.NUMBER),
|
||||
value: z.string().nullable(),
|
||||
value: z.number().nullable(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.EMAIL),
|
||||
|
||||
@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { SigningStatus } from '@prisma/client';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { ClockIcon, EyeOffIcon, LockIcon } from 'lucide-react';
|
||||
import { ClockIcon, EyeOffIcon } from 'lucide-react';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
@ -20,15 +20,7 @@ import { PopoverHover } from '../../primitives/popover';
|
||||
interface EnvelopeRecipientFieldTooltipProps {
|
||||
field: Pick<
|
||||
Field,
|
||||
| 'id'
|
||||
| 'inserted'
|
||||
| 'positionX'
|
||||
| 'positionY'
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'page'
|
||||
| 'type'
|
||||
| 'fieldMeta'
|
||||
'id' | 'inserted' | 'positionX' | 'positionY' | 'width' | 'height' | 'page' | 'type'
|
||||
> & {
|
||||
recipient: Pick<Recipient, 'name' | 'email' | 'signingStatus'>;
|
||||
};
|
||||
@ -159,19 +151,10 @@ export function EnvelopeRecipientFieldTooltip({
|
||||
<Badge
|
||||
className="mx-auto mb-1 py-0.5"
|
||||
variant={
|
||||
field?.fieldMeta?.readOnly
|
||||
? 'neutral'
|
||||
: field.recipient.signingStatus === SigningStatus.SIGNED
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
field.recipient.signingStatus === SigningStatus.SIGNED ? 'default' : 'secondary'
|
||||
}
|
||||
>
|
||||
{field?.fieldMeta?.readOnly ? (
|
||||
<>
|
||||
<LockIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Read Only</Trans>
|
||||
</>
|
||||
) : field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
||||
{field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
||||
<>
|
||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Signed</Trans>
|
||||
|
||||
@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'bg-background border-input ring-offset-background placeholder:text-muted-foreground/40 focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'bg-background border-input ring-offset-background placeholder:text-muted-foreground/40 focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
{
|
||||
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],
|
||||
|
||||
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover z-9999 text-popover-foreground animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
|
||||
'bg-popover text-popover-foreground animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||