Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2f5e570cf | |||
| 9fd9613076 | |||
| 0977c16e33 | |||
| 88d5a636c3 |
@ -1,5 +1,4 @@
|
||||
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';
|
||||
@ -28,49 +27,71 @@ 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, number | null>(
|
||||
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, string | 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),
|
||||
});
|
||||
|
||||
@ -7,6 +7,7 @@ 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';
|
||||
@ -39,7 +40,7 @@ export const EditorFieldDateForm = ({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign || 'left',
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ 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';
|
||||
@ -39,7 +40,7 @@ export const EditorFieldEmailForm = ({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign || 'left',
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -3,6 +3,10 @@ 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 {
|
||||
@ -107,6 +111,119 @@ 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,6 +6,7 @@ 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';
|
||||
@ -39,7 +40,7 @@ export const EditorFieldInitialsForm = ({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign || 'left',
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ 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';
|
||||
@ -39,7 +40,7 @@ export const EditorFieldNameForm = ({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign || 'left',
|
||||
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -6,6 +6,11 @@ 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';
|
||||
@ -31,9 +36,12 @@ 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({
|
||||
@ -43,6 +51,9 @@ const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
||||
numberFormat: true,
|
||||
fontSize: true,
|
||||
textAlign: true,
|
||||
lineHeight: true,
|
||||
letterSpacing: true,
|
||||
verticalAlign: true,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
minValue: true,
|
||||
@ -99,8 +110,11 @@ export const EditorFieldNumberForm = ({
|
||||
placeholder: value.placeholder || '',
|
||||
value: value.value || '',
|
||||
numberFormat: value.numberFormat || null,
|
||||
fontSize: value.fontSize || 14,
|
||||
textAlign: value.textAlign || 'left',
|
||||
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,
|
||||
required: value.required || false,
|
||||
readOnly: value.readOnly || false,
|
||||
minValue: value.minValue,
|
||||
@ -118,6 +132,10 @@ export const EditorFieldNumberForm = ({
|
||||
useEffect(() => {
|
||||
const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues);
|
||||
|
||||
if (formValues.readOnly && !formValues.value) {
|
||||
void form.trigger('value');
|
||||
}
|
||||
|
||||
if (validatedFormValues.success) {
|
||||
onValueChange({
|
||||
type: 'number',
|
||||
@ -130,10 +148,12 @@ export const EditorFieldNumberForm = ({
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<fieldset className="flex flex-col gap-2">
|
||||
<div className="flex w-full flex-row gap-x-4">
|
||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||
|
||||
<div className="flex w-full flex-row gap-x-4">
|
||||
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||
|
||||
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} />
|
||||
</div>
|
||||
|
||||
<EditorGenericLabelField formControl={form.control} />
|
||||
@ -204,6 +224,12 @@ 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,11 +5,8 @@ import { Trans } from '@lingui/react/macro';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import {
|
||||
DEFAULT_FIELD_FONT_SIZE,
|
||||
type TSignatureFieldMeta,
|
||||
ZSignatureFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '@documenso/lib/constants/pdf';
|
||||
import { 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';
|
||||
@ -35,7 +32,7 @@ export const EditorFieldSignatureForm = ({
|
||||
resolver: zodResolver(ZSignatureFieldFormSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
fontSize: value.fontSize || DEFAULT_SIGNATURE_TEXT_FONT_SIZE,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -3,11 +3,16 @@ 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 { z } from 'zod';
|
||||
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 TTextFieldMeta as TextFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
Form,
|
||||
@ -22,32 +27,36 @@ import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
|
||||
import {
|
||||
EditorGenericFontSizeField,
|
||||
EditorGenericLetterSpacingField,
|
||||
EditorGenericLineHeightField,
|
||||
EditorGenericReadOnlyField,
|
||||
EditorGenericRequiredField,
|
||||
EditorGenericTextAlignField,
|
||||
EditorGenericVerticalAlignField,
|
||||
} from './editor-field-generic-field-forms';
|
||||
|
||||
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'],
|
||||
},
|
||||
);
|
||||
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'],
|
||||
},
|
||||
);
|
||||
|
||||
type TTextFieldFormSchema = z.infer<typeof ZTextFieldFormSchema>;
|
||||
|
||||
@ -73,7 +82,10 @@ export const EditorFieldTextForm = ({
|
||||
text: value.text || '',
|
||||
characterLimit: value.characterLimit || 0,
|
||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||
textAlign: value.textAlign || 'left',
|
||||
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,
|
||||
required: value.required || false,
|
||||
readOnly: value.readOnly || false,
|
||||
},
|
||||
@ -89,6 +101,10 @@ export const EditorFieldTextForm = ({
|
||||
useEffect(() => {
|
||||
const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues);
|
||||
|
||||
if (formValues.readOnly && !formValues.text) {
|
||||
void form.trigger('text');
|
||||
}
|
||||
|
||||
if (validatedFormValues.success) {
|
||||
onValueChange({
|
||||
type: 'text',
|
||||
@ -101,10 +117,12 @@ export const EditorFieldTextForm = ({
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<fieldset className="flex flex-col gap-2">
|
||||
<div className="flex w-full flex-row gap-x-4">
|
||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||
|
||||
<div className="flex w-full flex-row gap-x-4">
|
||||
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||
|
||||
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} />
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
@ -182,17 +200,16 @@ export const EditorFieldTextForm = ({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
className="bg-background"
|
||||
placeholder={t`Field character limit`}
|
||||
placeholder={t`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) {
|
||||
@ -206,6 +223,12 @@ 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>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type DocumentData, DocumentStatus, type EnvelopeItem } from '@prisma/client';
|
||||
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client';
|
||||
import { DownloadIcon } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
@ -100,7 +100,14 @@ export const DocumentCertificateQRView = ({
|
||||
)}
|
||||
|
||||
{internalVersion === 2 ? (
|
||||
<EnvelopeRenderProvider envelope={{ envelopeItems }} token={token}>
|
||||
<EnvelopeRenderProvider
|
||||
envelope={{
|
||||
envelopeItems,
|
||||
status: DocumentStatus.COMPLETED,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
}}
|
||||
token={token}
|
||||
>
|
||||
<DocumentCertificateQrV2
|
||||
title={title}
|
||||
recipientCount={recipientCount}
|
||||
@ -130,7 +137,7 @@ export const DocumentCertificateQRView = ({
|
||||
envelopeItems={envelopeItems}
|
||||
token={token}
|
||||
trigger={
|
||||
<Button type="button" variant="outline" className="flex-1">
|
||||
<Button type="button" variant="outline" className="w-fit">
|
||||
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
@ -189,7 +196,7 @@ const DocumentCertificateQrV2 = ({
|
||||
envelopeItems={envelopeItems}
|
||||
token={token}
|
||||
trigger={
|
||||
<Button type="button" variant="outline" className="flex-1">
|
||||
<Button type="button" variant="outline" className="w-fit">
|
||||
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
|
||||
@ -7,6 +7,7 @@ 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;
|
||||
@ -40,6 +41,10 @@ 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]);
|
||||
|
||||
@ -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 } from '@prisma/client';
|
||||
import { FieldType, SigningStatus } from '@prisma/client';
|
||||
import { FileTextIcon } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -201,7 +201,10 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
envelope={envelope}
|
||||
token={undefined}
|
||||
fields={fieldsWithPlaceholders}
|
||||
recipients={envelope.recipients}
|
||||
recipients={envelope.recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
}))}
|
||||
overrideSettings={{
|
||||
mode: 'export',
|
||||
}}
|
||||
|
||||
@ -49,7 +49,7 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const { t } = useLingui();
|
||||
const { envelope, setLocalEnvelope, relativePath } = useCurrentEnvelopeEditor();
|
||||
const { envelope, setLocalEnvelope, relativePath, editorFields } = useCurrentEnvelopeEditor();
|
||||
const { maximumEnvelopeItemCount, remaining } = useLimits();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -165,9 +165,17 @@ 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 { type Recipient, SigningStatus } from '@prisma/client';
|
||||
import { DocumentStatus, type Recipient, SigningStatus } from '@prisma/client';
|
||||
import type Konva from 'konva';
|
||||
|
||||
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||
@ -19,6 +19,7 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const {
|
||||
envelopeStatus,
|
||||
currentEnvelopeItem,
|
||||
fields,
|
||||
recipients,
|
||||
@ -42,6 +43,10 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
const { _className, scale } = pageContext;
|
||||
|
||||
const localPageFields = useMemo((): GenericLocalField[] => {
|
||||
if (envelopeStatus === DocumentStatus.COMPLETED) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fields
|
||||
.filter(
|
||||
(field) =>
|
||||
@ -54,11 +59,20 @@ 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) => {
|
||||
@ -67,12 +81,8 @@ 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,
|
||||
@ -83,7 +93,6 @@ 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: '',
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { type Field, FieldType, RecipientRole, type Signature } from '@prisma/client';
|
||||
import {
|
||||
type Field,
|
||||
FieldType,
|
||||
type Recipient,
|
||||
RecipientRole,
|
||||
type Signature,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
import type Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import { match } from 'ts-pattern';
|
||||
@ -12,6 +19,7 @@ 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';
|
||||
@ -19,6 +27,7 @@ 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';
|
||||
@ -36,6 +45,10 @@ 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();
|
||||
@ -91,6 +104,36 @@ 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');
|
||||
@ -376,6 +419,46 @@ 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,
|
||||
@ -412,11 +495,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
* Initialize the Konva page canvas and all fields and interactions.
|
||||
*/
|
||||
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||
// Render the fields.
|
||||
for (const field of localPageFields) {
|
||||
renderFieldOnLayer(field);
|
||||
}
|
||||
|
||||
renderFields();
|
||||
currentPageLayer.batchDraw();
|
||||
};
|
||||
|
||||
@ -428,9 +507,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
return;
|
||||
}
|
||||
|
||||
localPageFields.forEach((field) => {
|
||||
renderFieldOnLayer(field);
|
||||
});
|
||||
renderFields();
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]);
|
||||
@ -446,9 +523,7 @@ export default function EnvelopeSignerPageRenderer() {
|
||||
// Rerender the whole page.
|
||||
pageLayer.current.destroyChildren();
|
||||
|
||||
localPageFields.forEach((field) => {
|
||||
renderFieldOnLayer(field);
|
||||
});
|
||||
renderFields();
|
||||
|
||||
pageLayer.current.batchDraw();
|
||||
}, [selectedAssistantRecipient]);
|
||||
@ -475,6 +550,15 @@ 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,11 +7,13 @@ 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'>;
|
||||
@ -43,6 +45,10 @@ 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: number | null;
|
||||
number: string | null;
|
||||
};
|
||||
|
||||
export const handleNumberFieldClick = async (
|
||||
|
||||
@ -106,5 +106,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "2.0.4"
|
||||
"version": "2.0.5"
|
||||
}
|
||||
|
||||
6
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@ -100,7 +100,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"dependencies": {
|
||||
"@cantoo/pdf-lib": "^2.5.2",
|
||||
"@documenso/api": "*",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
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';
|
||||
@ -13,11 +15,66 @@ export type FieldTestData = TFieldAndMeta & {
|
||||
signature?: string;
|
||||
};
|
||||
|
||||
const columnWidth = 19.125;
|
||||
const rowHeight = 6.7;
|
||||
export const signatureBase64Demo = `data:image/png;base64,${fs.readFileSync(
|
||||
path.join(__dirname, '../../../packages/assets/', 'logo_icon.png'),
|
||||
'base64',
|
||||
)}`;
|
||||
|
||||
const alignmentGridStartX = 31;
|
||||
const alignmentGridStartY = 19.02;
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
/**
|
||||
@ -31,10 +88,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'email',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(0, 0),
|
||||
customText: 'admin@documenso.com',
|
||||
},
|
||||
{
|
||||
@ -44,10 +98,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'email',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(0, 1),
|
||||
customText: 'admin@documenso.com',
|
||||
},
|
||||
{
|
||||
@ -58,10 +109,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'email',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(0, 2),
|
||||
customText: 'admin@documenso.com',
|
||||
},
|
||||
/**
|
||||
@ -75,10 +123,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'name',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(1, 0),
|
||||
customText: 'John Doe',
|
||||
},
|
||||
{
|
||||
@ -88,10 +133,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'name',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(1, 1),
|
||||
customText: 'John Doe',
|
||||
},
|
||||
{
|
||||
@ -102,10 +144,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'name',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(1, 2),
|
||||
customText: 'John Doe',
|
||||
},
|
||||
/**
|
||||
@ -119,10 +158,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'date',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(2, 0),
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -132,10 +168,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'date',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(2, 1),
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -146,10 +179,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'date',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(2, 2),
|
||||
customText: '123456789',
|
||||
},
|
||||
/**
|
||||
@ -163,10 +193,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'text',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(3, 0),
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -176,10 +203,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'text',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(3, 1),
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -190,10 +214,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'text',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(3, 2),
|
||||
customText: '123456789',
|
||||
},
|
||||
/**
|
||||
@ -207,10 +228,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'number',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(4, 0),
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -220,10 +238,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'number',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(4, 1),
|
||||
customText: '123456789',
|
||||
},
|
||||
{
|
||||
@ -234,10 +249,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'number',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(4, 2),
|
||||
customText: '123456789',
|
||||
},
|
||||
/**
|
||||
@ -251,10 +263,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'initials',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(5, 0),
|
||||
customText: 'JD',
|
||||
},
|
||||
{
|
||||
@ -264,10 +273,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'initials',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(5, 1),
|
||||
customText: 'JD',
|
||||
},
|
||||
{
|
||||
@ -278,10 +284,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'initials',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(5, 2),
|
||||
customText: 'JD',
|
||||
},
|
||||
/**
|
||||
@ -299,10 +302,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
],
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(6, 0),
|
||||
customText: '0',
|
||||
},
|
||||
{
|
||||
@ -312,15 +312,12 @@ 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,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '2',
|
||||
...calculatePositionPageOne(6, 1),
|
||||
customText: '',
|
||||
},
|
||||
{
|
||||
type: FieldType.RADIO,
|
||||
@ -330,15 +327,12 @@ 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,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '',
|
||||
...calculatePositionPageOne(6, 2),
|
||||
customText: '1',
|
||||
},
|
||||
/**
|
||||
* Row 8 Checkbox
|
||||
@ -355,10 +349,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
],
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(7, 0),
|
||||
customText: toCheckboxCustomText([0]),
|
||||
},
|
||||
{
|
||||
@ -368,15 +359,12 @@ 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,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: toCheckboxCustomText([1]),
|
||||
...calculatePositionPageOne(7, 1),
|
||||
customText: '',
|
||||
},
|
||||
{
|
||||
type: FieldType.CHECKBOX,
|
||||
@ -386,15 +374,12 @@ 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,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
customText: '',
|
||||
...calculatePositionPageOne(7, 2),
|
||||
customText: toCheckboxCustomText([1]),
|
||||
},
|
||||
/**
|
||||
* Row 8 Dropdown
|
||||
@ -407,10 +392,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'dropdown',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(8, 0),
|
||||
customText: 'Option 1',
|
||||
},
|
||||
{
|
||||
@ -420,10 +402,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'dropdown',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(8, 1),
|
||||
customText: 'Option 1',
|
||||
},
|
||||
{
|
||||
@ -434,10 +413,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'dropdown',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(8, 2),
|
||||
customText: 'Option 1',
|
||||
},
|
||||
/**
|
||||
@ -450,10 +426,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'signature',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(9, 0),
|
||||
customText: '',
|
||||
signature: 'My Signature',
|
||||
},
|
||||
@ -463,10 +436,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'signature',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(9, 1),
|
||||
customText: '',
|
||||
signature: 'My Signature',
|
||||
},
|
||||
@ -477,22 +447,295 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'signature',
|
||||
},
|
||||
page: 1,
|
||||
height: rowHeight,
|
||||
width: columnWidth,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
...calculatePositionPageOne(9, 2),
|
||||
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,6 +7,7 @@ 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;
|
||||
@ -37,7 +38,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
page: 2,
|
||||
...calculatePosition(0, 0),
|
||||
customText: '',
|
||||
signature: 'My Signature',
|
||||
signature: signatureBase64Demo,
|
||||
},
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
@ -47,7 +48,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
page: 2,
|
||||
...calculatePosition(1, 0),
|
||||
customText: '',
|
||||
signature: 'My Signature',
|
||||
signature: signatureBase64Demo,
|
||||
},
|
||||
{
|
||||
type: FieldType.SIGNATURE,
|
||||
@ -67,7 +68,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
page: 2,
|
||||
...calculatePosition(3, 0),
|
||||
customText: '',
|
||||
signature: 'My Signature',
|
||||
signature: 'My Signature super overflow maybe',
|
||||
},
|
||||
|
||||
/**
|
||||
@ -80,7 +81,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(0, 0, 'full'),
|
||||
customText: '123456789',
|
||||
customText: 'Hello world, this is some random text that I have written here',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -89,7 +90,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(1, 0),
|
||||
customText: '123456789123456789123456789123456789',
|
||||
customText: 'Some text that should overflow correctly',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -109,7 +110,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(3, 0),
|
||||
customText: '123456789',
|
||||
customText: 'Input should have a placeholder text when clicked',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -119,7 +120,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(3, 1),
|
||||
customText: '123456789',
|
||||
customText: 'Should have a label during editing and signing',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -129,7 +130,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(3, 2),
|
||||
customText: '123456789',
|
||||
customText: '',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
@ -139,20 +140,19 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(4, 0),
|
||||
customText: '123456789',
|
||||
customText: 'This is a required field',
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
fieldMeta: {
|
||||
type: 'text',
|
||||
readOnly: true,
|
||||
text: 'Readonly Value',
|
||||
text: 'Some Readonly Value',
|
||||
},
|
||||
page: 3,
|
||||
...calculatePosition(4, 1),
|
||||
customText: 'Readonly Value',
|
||||
customText: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* PAGE 4 NUMBER
|
||||
*/
|
||||
@ -241,10 +241,11 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
type: 'number',
|
||||
readOnly: true,
|
||||
value: '123456789',
|
||||
},
|
||||
page: 4,
|
||||
...calculatePosition(4, 1),
|
||||
customText: '123456789',
|
||||
customText: '',
|
||||
},
|
||||
|
||||
/**
|
||||
@ -285,6 +286,7 @@ 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' },
|
||||
@ -293,17 +295,18 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 5,
|
||||
...calculatePosition(2, 0),
|
||||
customText: '',
|
||||
customText: '2',
|
||||
},
|
||||
{
|
||||
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: false, value: 'Option 3' },
|
||||
{ id: 3, checked: true, value: 'Option 3' },
|
||||
],
|
||||
},
|
||||
page: 5,
|
||||
@ -358,7 +361,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 6,
|
||||
...calculatePosition(2, 0),
|
||||
customText: '',
|
||||
customText: toCheckboxCustomText([2]),
|
||||
},
|
||||
{
|
||||
type: FieldType.CHECKBOX,
|
||||
@ -368,7 +371,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
readOnly: true,
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
],
|
||||
},
|
||||
page: 6,
|
||||
@ -445,11 +448,11 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
fieldMeta: {
|
||||
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
|
||||
type: 'dropdown',
|
||||
defaultValue: 'Option 1',
|
||||
defaultValue: 'Option 2',
|
||||
},
|
||||
page: 7,
|
||||
...calculatePosition(1, 0),
|
||||
customText: 'Option 1',
|
||||
customText: 'Option 2',
|
||||
},
|
||||
{
|
||||
type: FieldType.DROPDOWN,
|
||||
@ -460,13 +463,14 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
},
|
||||
page: 7,
|
||||
...calculatePosition(2, 0),
|
||||
customText: 'Option 1',
|
||||
customText: 'Option 3',
|
||||
},
|
||||
{
|
||||
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 { formatAlignmentTestFields } from '../../../constants/field-alignment-pdf';
|
||||
import { ALIGNMENT_TEST_FIELDS } 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: formatAlignmentTestFields.map((field) => ({
|
||||
data: ALIGNMENT_TEST_FIELDS.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(
|
||||
formatAlignmentTestFields.length + FIELD_META_TEST_FIELDS.length,
|
||||
ALIGNMENT_TEST_FIELDS.length + FIELD_META_TEST_FIELDS.length,
|
||||
);
|
||||
expect(finalEnvelope.title).toBe('Envelope Full Field Test');
|
||||
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
|
||||
|
||||
@ -34,7 +34,34 @@ import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
test.skip('field placement visual regression', async ({ page }, testInfo) => {
|
||||
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 }, testInfo) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const envelope = await seedAlignmentTestDocument({
|
||||
@ -289,7 +316,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).toEqual(0);
|
||||
expect.soft(comparison).toBeLessThan(2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BIN
packages/app-tests/visual-regression/alignment-pdf-0.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
packages/app-tests/visual-regression/alignment-pdf-1.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
packages/app-tests/visual-regression/alignment-pdf-2.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
packages/app-tests/visual-regression/alignment-pdf-3.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
packages/app-tests/visual-regression/blank-certificate.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
packages/app-tests/visual-regression/field-meta-pdf-0.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
packages/app-tests/visual-regression/field-meta-pdf-1.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
packages/app-tests/visual-regression/field-meta-pdf-2.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
packages/app-tests/visual-regression/field-meta-pdf-3.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
packages/app-tests/visual-regression/field-meta-pdf-4.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
packages/app-tests/visual-regression/field-meta-pdf-5.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
packages/app-tests/visual-regression/field-meta-pdf-6.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
packages/app-tests/visual-regression/field-meta-pdf-7.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
@ -11,7 +11,7 @@ export const validateNumberField = (
|
||||
|
||||
const { minValue, maxValue, readOnly, required, numberFormat, fontSize } = fieldMeta || {};
|
||||
|
||||
if (numberFormat) {
|
||||
if (numberFormat && value.length > 0) {
|
||||
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 { Recipient } from '@prisma/client';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { FieldType } from '@prisma/client';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
@ -63,6 +63,8 @@ type UseEditorFieldsResponse = {
|
||||
// Selected recipient
|
||||
selectedRecipient: Recipient | null;
|
||||
setSelectedRecipient: (recipientId: number | null) => void;
|
||||
|
||||
resetForm: (fields?: Field[]) => void;
|
||||
};
|
||||
|
||||
export const useEditorFields = ({
|
||||
@ -72,24 +74,30 @@ 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: {
|
||||
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,
|
||||
}),
|
||||
),
|
||||
},
|
||||
defaultValues: generateDefaultValues(),
|
||||
resolver: zodResolver(ZEditorFieldsFormSchema),
|
||||
});
|
||||
|
||||
@ -272,6 +280,10 @@ export const useEditorFields = ({
|
||||
setSelectedRecipientId(foundRecipient?.id ?? null);
|
||||
};
|
||||
|
||||
const resetForm = (fields?: Field[]) => {
|
||||
form.reset(generateDefaultValues(fields));
|
||||
};
|
||||
|
||||
return {
|
||||
// Core state
|
||||
localFields,
|
||||
@ -295,6 +307,8 @@ export const useEditorFields = ({
|
||||
// Selected recipient
|
||||
selectedRecipient,
|
||||
setSelectedRecipient,
|
||||
|
||||
resetForm,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -107,6 +107,10 @@ export function usePageRenderer(renderFunction: RenderFunction) {
|
||||
stage: stage.current,
|
||||
pageLayer: pageLayer.current,
|
||||
});
|
||||
|
||||
void document.fonts.ready.then(function () {
|
||||
pageLayer.current?.batchDraw();
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@ -30,6 +30,8 @@ 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[];
|
||||
@ -44,7 +46,7 @@ type EnvelopeRenderProviderValue = {
|
||||
interface EnvelopeRenderProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
envelope: Pick<TEnvelope, 'envelopeItems'>;
|
||||
envelope: Pick<TEnvelope, 'envelopeItems' | 'status' | 'type'>;
|
||||
|
||||
/**
|
||||
* Optional fields which are passed down to renderers for custom rendering needs.
|
||||
@ -100,7 +102,7 @@ export const EnvelopeRenderProvider = ({
|
||||
// Indexed by documentDataId.
|
||||
const [files, setFiles] = useState<Record<string, FileData>>({});
|
||||
|
||||
const [currentItem, setItem] = useState<EnvelopeRenderItem | null>(null);
|
||||
const [currentItem, setCurrentItem] = useState<EnvelopeRenderItem | null>(null);
|
||||
|
||||
const [renderError, setRenderError] = useState<boolean>(false);
|
||||
|
||||
@ -163,11 +165,15 @@ export const EnvelopeRenderProvider = ({
|
||||
const setCurrentEnvelopeItem = (envelopeItemId: string) => {
|
||||
const foundItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId);
|
||||
|
||||
setItem(foundItem ?? null);
|
||||
setCurrentItem(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);
|
||||
}
|
||||
@ -203,6 +209,8 @@ export const EnvelopeRenderProvider = ({
|
||||
value={{
|
||||
getPdfBuffer,
|
||||
envelopeItems,
|
||||
envelopeStatus: envelope.status,
|
||||
envelopeType: envelope.type,
|
||||
currentEnvelopeItem: currentItem,
|
||||
setCurrentEnvelopeItem,
|
||||
fields: fields ?? [],
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DocumentData, Envelope, EnvelopeItem } from '@prisma/client';
|
||||
import type { DocumentData, Envelope, EnvelopeItem, Field } from '@prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
@ -182,80 +182,10 @@ export const sendDocument = async ({
|
||||
// Validate and autoinsert fields for V2 envelopes.
|
||||
if (envelope.internalVersion === 2) {
|
||||
for (const unknownField of envelope.fields) {
|
||||
const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
|
||||
const fieldToAutoInsert = extractFieldAutoInsertValues(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;
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
if (fieldToAutoInsert) {
|
||||
fieldsToAutoInsert.push(fieldToAutoInsert);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -387,3 +317,86 @@ 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;
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DROPDOWN) {
|
||||
const { defaultValue, values = [] } = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
if (defaultValue && values.some((value) => value.value === defaultValue)) {
|
||||
return {
|
||||
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) {
|
||||
return {
|
||||
fieldId,
|
||||
customText: toCheckboxCustomText(checkedIndices),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ 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';
|
||||
@ -144,6 +145,19 @@ 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 { ZFieldSchema } from '../../types/field';
|
||||
import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field';
|
||||
import { ZRecipientLiteSchema } from '../../types/recipient';
|
||||
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
@ -63,9 +63,11 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
rejectionReason: true,
|
||||
})
|
||||
.extend({
|
||||
fields: ZFieldSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
fields: ZEnvelopeFieldSchema.extend({
|
||||
signature: SignatureSchema.pick({
|
||||
signatureImageAsBase64: true,
|
||||
typedSignature: true,
|
||||
}).nullish(),
|
||||
}).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,11 +88,13 @@ export const addUserToOrganisation = async ({
|
||||
organisationId,
|
||||
organisationGroups,
|
||||
organisationMemberRole,
|
||||
bypassEmail = false,
|
||||
}: {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
organisationGroups: OrganisationGroup[];
|
||||
organisationMemberRole: OrganisationMemberRole;
|
||||
bypassEmail?: boolean;
|
||||
}) => {
|
||||
const organisationGroupToUse = organisationGroups.find(
|
||||
(group) =>
|
||||
@ -122,13 +124,15 @@ export const addUserToOrganisation = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-joined.email',
|
||||
payload: {
|
||||
organisationId,
|
||||
memberUserId: userId,
|
||||
},
|
||||
});
|
||||
if (!bypassEmail) {
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-joined.email',
|
||||
payload: {
|
||||
organisationId,
|
||||
memberUserId: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
@ -32,10 +32,8 @@ 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 insertedFields) {
|
||||
for (const field of fields) {
|
||||
renderField({
|
||||
scale: 1,
|
||||
field: {
|
||||
|
||||
@ -215,6 +215,12 @@ 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;
|
||||
});
|
||||
@ -468,19 +474,28 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
signingOrder: directTemplateRecipient.signingOrder,
|
||||
fields: {
|
||||
createMany: {
|
||||
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,
|
||||
})),
|
||||
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,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,9 +1,46 @@
|
||||
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 DEFAULT_FIELD_FONT_SIZE = 14;
|
||||
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 ZBaseFieldMeta = z.object({
|
||||
label: z.string().optional(),
|
||||
@ -50,8 +87,14 @@ export type TDateFieldMeta = z.infer<typeof ZDateFieldMeta>;
|
||||
export const ZTextFieldMeta = ZBaseFieldMeta.extend({
|
||||
type: z.literal('text'),
|
||||
text: z.string().optional(),
|
||||
characterLimit: z.number().optional(),
|
||||
characterLimit: z.coerce
|
||||
.number({ invalid_type_error: msg`Value must be a number`.id })
|
||||
.min(0)
|
||||
.optional(),
|
||||
textAlign: ZFieldTextAlignSchema.optional(),
|
||||
lineHeight: ZFieldMetaLineHeight.nullish(),
|
||||
letterSpacing: ZFieldMetaLetterSpacing.nullish(),
|
||||
verticalAlign: ZFieldMetaVerticalAlign.nullish(),
|
||||
});
|
||||
|
||||
export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
|
||||
@ -63,6 +106,9 @@ 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 } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode, color } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
@ -210,7 +210,9 @@ export const renderCheckboxFieldElement = (
|
||||
fieldGroup.add(text);
|
||||
});
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -50,7 +50,7 @@ export const renderDropdownFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { pageWidth, pageHeight, pageLayer, mode, translations } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode, translations, color } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
@ -74,6 +74,21 @@ 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;
|
||||
}
|
||||
@ -166,7 +181,9 @@ export const renderDropdownFieldElement = (
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -77,6 +77,7 @@ export const renderField = ({
|
||||
scale,
|
||||
};
|
||||
|
||||
// If the generic text field element array changes, update the `GenericTextFieldTypeMetas` type
|
||||
return match(field.type)
|
||||
.with(
|
||||
FieldType.INITIALS,
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import Konva from 'konva';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { TTextFieldMeta } from '../../types/field-meta';
|
||||
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 {
|
||||
createFieldHoverInteraction,
|
||||
konvaTextFill,
|
||||
@ -12,14 +18,14 @@ import {
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
|
||||
const DEFAULT_TEXT_ALIGN = 'left';
|
||||
const DEFAULT_TEXT_X_PADDING = 6;
|
||||
|
||||
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
||||
|
||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||
|
||||
const textMeta = field.fieldMeta as TTextFieldMeta | undefined;
|
||||
const fieldMeta = field.fieldMeta as GenericTextFieldTypeMetas | undefined;
|
||||
|
||||
const fieldTypeName = translations?.[field.type] || field.type;
|
||||
|
||||
@ -33,53 +39,62 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
// Calculate text positioning based on alignment
|
||||
const textX = 0;
|
||||
const textY = 0;
|
||||
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;
|
||||
const textFontSize = fieldMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
|
||||
let textToRender: string = fieldTypeName;
|
||||
// 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;
|
||||
|
||||
// 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 this we want to ensure we don't show
|
||||
// any placeholder text or labels unless actually it's inserted.
|
||||
if (mode === 'export') {
|
||||
textToRender = '';
|
||||
}
|
||||
|
||||
// Use default values for text/number if provided.
|
||||
if (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 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';
|
||||
}
|
||||
}
|
||||
if (field.inserted) {
|
||||
textToRender = field.customText;
|
||||
|
||||
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,
|
||||
x: textX + DEFAULT_TEXT_X_PADDING,
|
||||
y: textY,
|
||||
verticalAlign: textVerticalAlign,
|
||||
wrap: 'word',
|
||||
padding: textPadding,
|
||||
text: textToRender,
|
||||
fontSize: textFontSize,
|
||||
align: textAlign,
|
||||
lineHeight: textLineHeight,
|
||||
letterSpacing: textLetterSpacing,
|
||||
fontFamily: konvaTextFontFamily,
|
||||
fill: konvaTextFill,
|
||||
align: textAlign,
|
||||
width: fieldWidth,
|
||||
width: fieldWidth - DEFAULT_TEXT_X_PADDING * 2,
|
||||
height: fieldHeight,
|
||||
} satisfies Partial<Konva.TextConfig>);
|
||||
|
||||
@ -90,7 +105,7 @@ export const renderGenericTextFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { mode = 'edit', pageLayer } = options;
|
||||
const { mode = 'edit', pageLayer, color } = options;
|
||||
|
||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||
|
||||
@ -125,7 +140,7 @@ export const renderGenericTextFieldElement = (
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth);
|
||||
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2);
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
@ -143,7 +158,7 @@ export const renderGenericTextFieldElement = (
|
||||
const rectHeight = fieldRect.height();
|
||||
|
||||
// Update text dimensions
|
||||
fieldText.width(rectWidth); // Account for padding
|
||||
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2);
|
||||
fieldText.height(rectHeight);
|
||||
|
||||
// Force Konva to recalculate text layout
|
||||
@ -158,7 +173,9 @@ export const renderGenericTextFieldElement = (
|
||||
fieldRect.opacity(0);
|
||||
}
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -25,7 +25,7 @@ export const renderRadioFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { pageWidth, pageHeight, pageLayer, mode } = options;
|
||||
const { pageWidth, pageHeight, pageLayer, mode, color } = options;
|
||||
|
||||
const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
|
||||
const radioValues = radioMeta?.values || [];
|
||||
@ -195,7 +195,9 @@ export const renderRadioFieldElement = (
|
||||
fieldGroup.add(text);
|
||||
});
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -142,7 +142,7 @@ export const renderSignatureFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
const { mode = 'edit', pageLayer } = options;
|
||||
const { mode = 'edit', pageLayer, color } = options;
|
||||
|
||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||
|
||||
@ -211,7 +211,9 @@ export const renderSignatureFieldElement = (
|
||||
fieldRect.opacity(0);
|
||||
}
|
||||
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
if (color !== 'readOnly' && mode !== 'export') {
|
||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||
}
|
||||
|
||||
return {
|
||||
fieldGroup,
|
||||
|
||||
@ -102,7 +102,7 @@ export const extractFieldInsertionValues = ({
|
||||
}
|
||||
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true);
|
||||
const errors = validateNumberField(fieldValue.value, numberFieldParsedMeta, true);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
@ -111,7 +111,7 @@ export const extractFieldInsertionValues = ({
|
||||
}
|
||||
|
||||
return {
|
||||
customText: fieldValue.value.toString(),
|
||||
customText: fieldValue.value,
|
||||
inserted: true,
|
||||
};
|
||||
})
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { formatAlignmentTestFields } from '@documenso/app-tests/constants/field-alignment-pdf';
|
||||
import { ALIGNMENT_TEST_FIELDS } 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 } from '@documenso/lib/server-only/envelope/increment-id';
|
||||
import {
|
||||
incrementDocumentId,
|
||||
incrementTemplateId,
|
||||
} 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,
|
||||
@ -176,6 +183,26 @@ 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,
|
||||
@ -192,6 +219,26 @@ 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,
|
||||
@ -214,17 +261,25 @@ 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'))
|
||||
@ -237,7 +292,10 @@ export const seedAlignmentTestDocument = async ({
|
||||
const alignmentDocumentData = await createDocumentData({ documentData: alignmentPdf });
|
||||
const fieldMetaDocumentData = await createDocumentData({ documentData: fieldMetaPdf });
|
||||
|
||||
const documentId = await incrementDocumentId();
|
||||
const secondaryId =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
||||
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: {},
|
||||
@ -246,12 +304,12 @@ export const seedAlignmentTestDocument = async ({
|
||||
const createdEnvelope = await prisma.envelope.create({
|
||||
data: {
|
||||
id: prefixedId('envelope'),
|
||||
secondaryId: documentId.formattedDocumentId,
|
||||
secondaryId,
|
||||
internalVersion: 2,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
type,
|
||||
documentMetaId: documentMeta.id,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
title: `Envelope Full Field Test`,
|
||||
title,
|
||||
status,
|
||||
envelopeItems: {
|
||||
createMany: {
|
||||
@ -275,8 +333,8 @@ export const seedAlignmentTestDocument = async ({
|
||||
teamId,
|
||||
recipients: {
|
||||
create: {
|
||||
name: recipientName,
|
||||
email: recipientEmail,
|
||||
name: isDirectTemplate ? DIRECT_TEMPLATE_RECIPIENT_NAME : recipientName,
|
||||
email: isDirectTemplate ? DIRECT_TEMPLATE_RECIPIENT_EMAIL : recipientEmail,
|
||||
token: nanoid(),
|
||||
sendStatus: status === 'DRAFT' ? SendStatus.NOT_SENT : SendStatus.SENT,
|
||||
signingStatus: status === 'COMPLETED' ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
@ -292,6 +350,25 @@ 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;
|
||||
@ -301,7 +378,7 @@ export const seedAlignmentTestDocument = async ({
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
formatAlignmentTestFields.map(async (field) => {
|
||||
ALIGNMENT_TEST_FIELDS.map(async (field) => {
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
...field,
|
||||
@ -309,16 +386,20 @@ export const seedAlignmentTestDocument = async ({
|
||||
envelopeItemId: envelopeItemAlignmentItem,
|
||||
envelopeId: id,
|
||||
customText: insertFields ? field.customText : '',
|
||||
inserted: insertFields,
|
||||
signature: field.signature
|
||||
? {
|
||||
create: {
|
||||
recipientId,
|
||||
signatureImageAsBase64: isBase64Image(field.signature) ? field.signature : null,
|
||||
typedSignature: isBase64Image(field.signature) ? null : field.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
@ -333,16 +414,20 @@ export const seedAlignmentTestDocument = async ({
|
||||
envelopeItemId: envelopeItemFieldMetaItem,
|
||||
envelopeId: id,
|
||||
customText: insertFields ? field.customText : '',
|
||||
inserted: insertFields,
|
||||
signature: field.signature
|
||||
? {
|
||||
create: {
|
||||
recipientId,
|
||||
signatureImageAsBase64: isBase64Image(field.signature) ? field.signature : null,
|
||||
typedSignature: isBase64Image(field.signature) ? null : field.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@ -64,6 +64,7 @@ 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.number().nullable(),
|
||||
value: z.string().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 } from 'lucide-react';
|
||||
import { ClockIcon, EyeOffIcon, LockIcon } 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,7 +20,15 @@ import { PopoverHover } from '../../primitives/popover';
|
||||
interface EnvelopeRecipientFieldTooltipProps {
|
||||
field: Pick<
|
||||
Field,
|
||||
'id' | 'inserted' | 'positionX' | 'positionY' | 'width' | 'height' | 'page' | 'type'
|
||||
| 'id'
|
||||
| 'inserted'
|
||||
| 'positionX'
|
||||
| 'positionY'
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'page'
|
||||
| 'type'
|
||||
| 'fieldMeta'
|
||||
> & {
|
||||
recipient: Pick<Recipient, 'name' | 'email' | 'signingStatus'>;
|
||||
};
|
||||
@ -151,10 +159,19 @@ export function EnvelopeRecipientFieldTooltip({
|
||||
<Badge
|
||||
className="mx-auto mb-1 py-0.5"
|
||||
variant={
|
||||
field.recipient.signingStatus === SigningStatus.SIGNED ? 'default' : 'secondary'
|
||||
field?.fieldMeta?.readOnly
|
||||
? 'neutral'
|
||||
: field.recipient.signingStatus === SigningStatus.SIGNED
|
||||
? 'default'
|
||||
: 'secondary'
|
||||
}
|
||||
>
|
||||
{field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
||||
{field?.fieldMeta?.readOnly ? (
|
||||
<>
|
||||
<LockIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Read Only</Trans>
|
||||
</>
|
||||
) : field.recipient.signingStatus === SigningStatus.SIGNED ? (
|
||||
<>
|
||||
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Signed</Trans>
|
||||
|
||||