Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff44ffbc03 | |||
| 441842d2bd | |||
| ca0b83579f | |||
| 6c0d1da91e | |||
| 805982f3e8 | |||
| e2f5e570cf | |||
| 9fd9613076 | |||
| 0977c16e33 | |||
| 88d5a636c3 | |||
| 1e6292b1d9 | |||
| d65866156d | |||
| fe8915162f | |||
| 37a2634aca | |||
| eff7d90f43 | |||
| db5524f8ce | |||
| 3d539b20ad | |||
| 48626b9169 | |||
| 88371b665a |
@ -336,7 +336,7 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
<Trans>Message</Trans>{' '}
|
<Trans>Message</Trans>{' '}
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-muted-foreground p-4">
|
<TooltipContent className="text-muted-foreground p-4">
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { createCallable } from 'react-call';
|
import { createCallable } from 'react-call';
|
||||||
@ -28,49 +27,71 @@ import {
|
|||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
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 = {
|
export type SignFieldNumberDialogProps = {
|
||||||
fieldMeta: TNumberFieldMeta;
|
fieldMeta: TNumberFieldMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, number | null>(
|
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, string | null>(
|
||||||
({ call, fieldMeta }) => {
|
({ call, fieldMeta }) => {
|
||||||
const { t } = useLingui();
|
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({
|
const ZSignFieldNumberFormSchema = z.object({
|
||||||
number: createNumberFieldSchema(fieldMeta),
|
number: createNumberFieldSchema(fieldMeta),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -336,7 +336,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
envelopeItem={envelopeItems[0]}
|
envelopeItem={envelopeItems[0]}
|
||||||
token={token}
|
token={recipient.token}
|
||||||
version="signed"
|
version="signed"
|
||||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type { z } from 'zod';
|
|||||||
import {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
type TDateFieldMeta as DateFieldMeta,
|
type TDateFieldMeta as DateFieldMeta,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
ZDateFieldMeta,
|
ZDateFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import { Form } from '@documenso/ui/primitives/form/form';
|
import { Form } from '@documenso/ui/primitives/form/form';
|
||||||
@ -39,7 +40,7 @@ export const EditorFieldDateForm = ({
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
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 {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
type TEmailFieldMeta as EmailFieldMeta,
|
type TEmailFieldMeta as EmailFieldMeta,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
ZEmailFieldMeta,
|
ZEmailFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import { Form } from '@documenso/ui/primitives/form/form';
|
import { Form } from '@documenso/ui/primitives/form/form';
|
||||||
@ -39,7 +40,7 @@ export const EditorFieldEmailForm = ({
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
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 { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { type Control, useFormContext } from 'react-hook-form';
|
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 { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||||
import {
|
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 = ({
|
export const EditorGenericRequiredField = ({
|
||||||
formControl,
|
formControl,
|
||||||
className,
|
className,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type { z } from 'zod';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
type TInitialsFieldMeta as InitialsFieldMeta,
|
type TInitialsFieldMeta as InitialsFieldMeta,
|
||||||
ZInitialsFieldMeta,
|
ZInitialsFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
@ -39,7 +40,7 @@ export const EditorFieldInitialsForm = ({
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
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 {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
type TNameFieldMeta as NameFieldMeta,
|
type TNameFieldMeta as NameFieldMeta,
|
||||||
ZNameFieldMeta,
|
ZNameFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
@ -39,7 +40,7 @@ export const EditorFieldNameForm = ({
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
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 type { z } from 'zod';
|
||||||
|
|
||||||
import {
|
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,
|
type TNumberFieldMeta as NumberFieldMeta,
|
||||||
ZNumberFieldMeta,
|
ZNumberFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
@ -31,9 +36,12 @@ import { Separator } from '@documenso/ui/primitives/separator';
|
|||||||
import {
|
import {
|
||||||
EditorGenericFontSizeField,
|
EditorGenericFontSizeField,
|
||||||
EditorGenericLabelField,
|
EditorGenericLabelField,
|
||||||
|
EditorGenericLetterSpacingField,
|
||||||
|
EditorGenericLineHeightField,
|
||||||
EditorGenericReadOnlyField,
|
EditorGenericReadOnlyField,
|
||||||
EditorGenericRequiredField,
|
EditorGenericRequiredField,
|
||||||
EditorGenericTextAlignField,
|
EditorGenericTextAlignField,
|
||||||
|
EditorGenericVerticalAlignField,
|
||||||
} from './editor-field-generic-field-forms';
|
} from './editor-field-generic-field-forms';
|
||||||
|
|
||||||
const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
||||||
@ -43,6 +51,9 @@ const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
|||||||
numberFormat: true,
|
numberFormat: true,
|
||||||
fontSize: true,
|
fontSize: true,
|
||||||
textAlign: true,
|
textAlign: true,
|
||||||
|
lineHeight: true,
|
||||||
|
letterSpacing: true,
|
||||||
|
verticalAlign: true,
|
||||||
required: true,
|
required: true,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
minValue: true,
|
minValue: true,
|
||||||
@ -99,8 +110,11 @@ export const EditorFieldNumberForm = ({
|
|||||||
placeholder: value.placeholder || '',
|
placeholder: value.placeholder || '',
|
||||||
value: value.value || '',
|
value: value.value || '',
|
||||||
numberFormat: value.numberFormat || null,
|
numberFormat: value.numberFormat || null,
|
||||||
fontSize: value.fontSize || 14,
|
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,
|
required: value.required || false,
|
||||||
readOnly: value.readOnly || false,
|
readOnly: value.readOnly || false,
|
||||||
minValue: value.minValue,
|
minValue: value.minValue,
|
||||||
@ -118,6 +132,10 @@ export const EditorFieldNumberForm = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues);
|
const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues);
|
||||||
|
|
||||||
|
if (formValues.readOnly && !formValues.value) {
|
||||||
|
void form.trigger('value');
|
||||||
|
}
|
||||||
|
|
||||||
if (validatedFormValues.success) {
|
if (validatedFormValues.success) {
|
||||||
onValueChange({
|
onValueChange({
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@ -130,10 +148,12 @@ export const EditorFieldNumberForm = ({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form>
|
<form>
|
||||||
<fieldset className="flex flex-col gap-2">
|
<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} />
|
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||||
|
|
||||||
|
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditorGenericLabelField formControl={form.control} />
|
<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">
|
<div className="mt-1">
|
||||||
<EditorGenericRequiredField formControl={form.control} />
|
<EditorGenericRequiredField formControl={form.control} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,11 +5,8 @@ import { Trans } from '@lingui/react/macro';
|
|||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '@documenso/lib/constants/pdf';
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
import { type TSignatureFieldMeta, ZSignatureFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
type TSignatureFieldMeta,
|
|
||||||
ZSignatureFieldMeta,
|
|
||||||
} from '@documenso/lib/types/field-meta';
|
|
||||||
import { Form } from '@documenso/ui/primitives/form/form';
|
import { Form } from '@documenso/ui/primitives/form/form';
|
||||||
|
|
||||||
import { EditorGenericFontSizeField } from './editor-field-generic-field-forms';
|
import { EditorGenericFontSizeField } from './editor-field-generic-field-forms';
|
||||||
@ -35,7 +32,7 @@ export const EditorFieldSignatureForm = ({
|
|||||||
resolver: zodResolver(ZSignatureFieldFormSchema),
|
resolver: zodResolver(ZSignatureFieldFormSchema),
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
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 { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
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,
|
type TTextFieldMeta as TextFieldMeta,
|
||||||
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -22,23 +27,27 @@ import { Textarea } from '@documenso/ui/primitives/textarea';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
EditorGenericFontSizeField,
|
EditorGenericFontSizeField,
|
||||||
|
EditorGenericLetterSpacingField,
|
||||||
|
EditorGenericLineHeightField,
|
||||||
EditorGenericReadOnlyField,
|
EditorGenericReadOnlyField,
|
||||||
EditorGenericRequiredField,
|
EditorGenericRequiredField,
|
||||||
EditorGenericTextAlignField,
|
EditorGenericTextAlignField,
|
||||||
|
EditorGenericVerticalAlignField,
|
||||||
} from './editor-field-generic-field-forms';
|
} from './editor-field-generic-field-forms';
|
||||||
|
|
||||||
const ZTextFieldFormSchema = z
|
const ZTextFieldFormSchema = ZTextFieldMeta.pick({
|
||||||
.object({
|
label: true,
|
||||||
label: z.string().optional(),
|
placeholder: true,
|
||||||
placeholder: z.string().optional(),
|
text: true,
|
||||||
text: z.string().optional(),
|
characterLimit: true,
|
||||||
characterLimit: z.coerce.number().min(0).optional(),
|
fontSize: true,
|
||||||
fontSize: z.coerce.number().min(8).max(96).optional(),
|
textAlign: true,
|
||||||
textAlign: z.enum(['left', 'center', 'right']).optional(),
|
lineHeight: true,
|
||||||
required: z.boolean().optional(),
|
letterSpacing: true,
|
||||||
readOnly: z.boolean().optional(),
|
verticalAlign: true,
|
||||||
})
|
required: true,
|
||||||
.refine(
|
readOnly: true,
|
||||||
|
}).refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// A read-only field must have text
|
// A read-only field must have text
|
||||||
return !data.readOnly || (data.text && data.text.length > 0);
|
return !data.readOnly || (data.text && data.text.length > 0);
|
||||||
@ -73,7 +82,10 @@ export const EditorFieldTextForm = ({
|
|||||||
text: value.text || '',
|
text: value.text || '',
|
||||||
characterLimit: value.characterLimit || 0,
|
characterLimit: value.characterLimit || 0,
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
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,
|
required: value.required || false,
|
||||||
readOnly: value.readOnly || false,
|
readOnly: value.readOnly || false,
|
||||||
},
|
},
|
||||||
@ -89,6 +101,10 @@ export const EditorFieldTextForm = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues);
|
const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues);
|
||||||
|
|
||||||
|
if (formValues.readOnly && !formValues.text) {
|
||||||
|
void form.trigger('text');
|
||||||
|
}
|
||||||
|
|
||||||
if (validatedFormValues.success) {
|
if (validatedFormValues.success) {
|
||||||
onValueChange({
|
onValueChange({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@ -101,10 +117,12 @@ export const EditorFieldTextForm = ({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form>
|
<form>
|
||||||
<fieldset className="flex flex-col gap-2">
|
<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} />
|
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||||
|
|
||||||
|
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@ -182,17 +200,16 @@ export const EditorFieldTextForm = ({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
placeholder={t`Field character limit`}
|
placeholder={t`Character limit`}
|
||||||
{...field}
|
{...field}
|
||||||
|
value={field.value || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
field.onChange(e);
|
|
||||||
|
|
||||||
const values = form.getValues();
|
const values = form.getValues();
|
||||||
const characterLimit = parseInt(e.target.value, 10) || 0;
|
const characterLimit = parseInt(e.target.value, 10) || 0;
|
||||||
|
|
||||||
|
field.onChange(characterLimit || '');
|
||||||
|
|
||||||
const textValue = values.text || '';
|
const textValue = values.text || '';
|
||||||
|
|
||||||
if (characterLimit > 0 && textValue.length > characterLimit) {
|
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">
|
<div className="mt-1">
|
||||||
<EditorGenericRequiredField formControl={form.control} />
|
<EditorGenericRequiredField formControl={form.control} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -184,10 +184,10 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="embed--DocumentWidgetFooter">
|
<div className="embed--DocumentWidgetFooter mt-auto">
|
||||||
{/* Footer of left sidebar. */}
|
{/* Footer of left sidebar. */}
|
||||||
{!isEmbed && (
|
{!isEmbed && (
|
||||||
<div className="mt-auto px-4">
|
<div className="px-4">
|
||||||
<Button asChild variant="ghost" className="w-full justify-start">
|
<Button asChild variant="ghost" className="w-full justify-start">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<ArrowLeftIcon className="mr-2 h-4 w-4" />
|
<ArrowLeftIcon className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
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 { DownloadIcon } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
@ -100,7 +100,14 @@ export const DocumentCertificateQRView = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{internalVersion === 2 ? (
|
{internalVersion === 2 ? (
|
||||||
<EnvelopeRenderProvider envelope={{ envelopeItems }} token={token}>
|
<EnvelopeRenderProvider
|
||||||
|
envelope={{
|
||||||
|
envelopeItems,
|
||||||
|
status: DocumentStatus.COMPLETED,
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
}}
|
||||||
|
token={token}
|
||||||
|
>
|
||||||
<DocumentCertificateQrV2
|
<DocumentCertificateQrV2
|
||||||
title={title}
|
title={title}
|
||||||
recipientCount={recipientCount}
|
recipientCount={recipientCount}
|
||||||
@ -130,7 +137,7 @@ export const DocumentCertificateQRView = ({
|
|||||||
envelopeItems={envelopeItems}
|
envelopeItems={envelopeItems}
|
||||||
token={token}
|
token={token}
|
||||||
trigger={
|
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" />
|
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||||
<Trans>Download</Trans>
|
<Trans>Download</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
@ -189,7 +196,7 @@ const DocumentCertificateQrV2 = ({
|
|||||||
envelopeItems={envelopeItems}
|
envelopeItems={envelopeItems}
|
||||||
token={token}
|
token={token}
|
||||||
trigger={
|
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" />
|
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||||
<Trans>Download</Trans>
|
<Trans>Download</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { DateTime } from 'luxon';
|
|||||||
|
|
||||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||||
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
|
|
||||||
export type DocumentPageViewInformationProps = {
|
export type DocumentPageViewInformationProps = {
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -40,6 +41,10 @@ export const DocumentPageViewInformation = ({
|
|||||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
.toRelative(),
|
.toRelative(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: msg`Document ID (Legacy)`,
|
||||||
|
value: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isMounted, envelope, userId]);
|
}, [isMounted, envelope, userId]);
|
||||||
|
|||||||
@ -616,13 +616,14 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
zIndex: 50,
|
zIndex: 50,
|
||||||
}}
|
}}
|
||||||
className="text-muted-foreground grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border bg-white p-1 shadow-sm"
|
// Don't use darkmode for this component, it should look the same for both light/dark modes.
|
||||||
|
className="grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border border-gray-300 bg-white p-1 text-gray-500 shadow-sm"
|
||||||
>
|
>
|
||||||
{fieldButtonList.map((field) => (
|
{fieldButtonList.map((field) => (
|
||||||
<button
|
<button
|
||||||
key={field.type}
|
key={field.type}
|
||||||
onClick={() => createFieldFromPendingTemplate(pendingFieldCreation, field.type)}
|
onClick={() => createFieldFromPendingTemplate(pendingFieldCreation, field.type)}
|
||||||
className="hover:text-foreground col-span-1 w-full flex-shrink-0 rounded-sm px-2 py-1 text-xs hover:bg-gray-100"
|
className="col-span-1 w-full flex-shrink-0 rounded-sm px-2 py-1 text-xs hover:bg-gray-100 hover:text-gray-600"
|
||||||
>
|
>
|
||||||
{t(field.name)}
|
{t(field.name)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { lazy, useEffect, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { faker } from '@faker-js/faker/locale/en';
|
import { faker } from '@faker-js/faker/locale/en';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { FieldType } from '@prisma/client';
|
import { FieldType, SigningStatus } from '@prisma/client';
|
||||||
import { FileTextIcon } from 'lucide-react';
|
import { FileTextIcon } from 'lucide-react';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@ -201,7 +201,10 @@ export const EnvelopeEditorPreviewPage = () => {
|
|||||||
envelope={envelope}
|
envelope={envelope}
|
||||||
token={undefined}
|
token={undefined}
|
||||||
fields={fieldsWithPlaceholders}
|
fields={fieldsWithPlaceholders}
|
||||||
recipients={envelope.recipients}
|
recipients={envelope.recipients.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
}))}
|
||||||
overrideSettings={{
|
overrideSettings={{
|
||||||
mode: 'export',
|
mode: 'export',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -212,7 +212,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hasDocumentBeenSent = recipients.some(
|
const hasDocumentBeenSent = recipients.some(
|
||||||
(recipient) => recipient.sendStatus === SendStatus.SENT,
|
(recipient) => recipient.role !== RecipientRole.CC && recipient.sendStatus === SendStatus.SENT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const canRecipientBeModified = (recipientId?: number) => {
|
const canRecipientBeModified = (recipientId?: number) => {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
const organisation = useCurrentOrganisation();
|
const organisation = useCurrentOrganisation();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const { envelope, setLocalEnvelope, relativePath } = useCurrentEnvelopeEditor();
|
const { envelope, setLocalEnvelope, relativePath, editorFields } = useCurrentEnvelopeEditor();
|
||||||
const { maximumEnvelopeItemCount, remaining } = useLimits();
|
const { maximumEnvelopeItemCount, remaining } = useLimits();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -165,9 +165,17 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
const onFileDelete = (envelopeItemId: string) => {
|
const onFileDelete = (envelopeItemId: string) => {
|
||||||
setLocalFiles((prev) => prev.filter((uploadingFile) => uploadingFile.id !== envelopeItemId));
|
setLocalFiles((prev) => prev.filter((uploadingFile) => uploadingFile.id !== envelopeItemId));
|
||||||
|
|
||||||
|
const fieldsWithoutDeletedItem = envelope.fields.filter(
|
||||||
|
(field) => field.envelopeItemId !== envelopeItemId,
|
||||||
|
);
|
||||||
|
|
||||||
setLocalEnvelope({
|
setLocalEnvelope({
|
||||||
envelopeItems: envelope.envelopeItems.filter((item) => item.id !== envelopeItemId),
|
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 { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
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 type Konva from 'konva';
|
||||||
|
|
||||||
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||||
@ -19,6 +19,7 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
const { i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
envelopeStatus,
|
||||||
currentEnvelopeItem,
|
currentEnvelopeItem,
|
||||||
fields,
|
fields,
|
||||||
recipients,
|
recipients,
|
||||||
@ -42,6 +43,10 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
const { _className, scale } = pageContext;
|
const { _className, scale } = pageContext;
|
||||||
|
|
||||||
const localPageFields = useMemo((): GenericLocalField[] => {
|
const localPageFields = useMemo((): GenericLocalField[] => {
|
||||||
|
if (envelopeStatus === DocumentStatus.COMPLETED) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
.filter(
|
.filter(
|
||||||
(field) =>
|
(field) =>
|
||||||
@ -54,11 +59,20 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
throw new Error(`Recipient not found for field ${field.id}`);
|
throw new Error(`Recipient not found for field ${field.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInserted = recipient.signingStatus === SigningStatus.SIGNED && field.inserted;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
|
inserted: isInserted,
|
||||||
|
customText: isInserted ? field.customText : '',
|
||||||
recipient,
|
recipient,
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
.filter(
|
||||||
|
({ inserted, fieldMeta, recipient }) =>
|
||||||
|
(recipient.signingStatus === SigningStatus.SIGNED ? inserted : true) ||
|
||||||
|
fieldMeta?.readOnly,
|
||||||
|
);
|
||||||
}, [fields, pageContext.pageNumber, currentEnvelopeItem?.id, recipients]);
|
}, [fields, pageContext.pageNumber, currentEnvelopeItem?.id, recipients]);
|
||||||
|
|
||||||
const unsafeRenderFieldOnLayer = (field: GenericLocalField) => {
|
const unsafeRenderFieldOnLayer = (field: GenericLocalField) => {
|
||||||
@ -67,12 +81,8 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { recipient } = field;
|
|
||||||
|
|
||||||
const fieldTranslations = getClientSideFieldTranslations(i18n);
|
const fieldTranslations = getClientSideFieldTranslations(i18n);
|
||||||
|
|
||||||
const isInserted = recipient.signingStatus === SigningStatus.SIGNED && field.inserted;
|
|
||||||
|
|
||||||
renderField({
|
renderField({
|
||||||
scale,
|
scale,
|
||||||
pageLayer: pageLayer.current,
|
pageLayer: pageLayer.current,
|
||||||
@ -83,7 +93,6 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
height: Number(field.height),
|
height: Number(field.height),
|
||||||
positionX: Number(field.positionX),
|
positionX: Number(field.positionX),
|
||||||
positionY: Number(field.positionY),
|
positionY: Number(field.positionY),
|
||||||
customText: isInserted ? field.customText : '',
|
|
||||||
fieldMeta: field.fieldMeta,
|
fieldMeta: field.fieldMeta,
|
||||||
signature: {
|
signature: {
|
||||||
signatureImageAsBase64: '',
|
signatureImageAsBase64: '',
|
||||||
@ -95,7 +104,7 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
pageHeight: unscaledViewport.height,
|
pageHeight: unscaledViewport.height,
|
||||||
color: getRecipientColorKey(field.recipientId),
|
color: getRecipientColorKey(field.recipientId),
|
||||||
editable: false,
|
editable: false,
|
||||||
mode: overrideSettings?.mode ?? 'sign',
|
mode: overrideSettings?.mode ?? 'edit',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
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 Konva from 'konva';
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import { match } from 'ts-pattern';
|
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 { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
|
||||||
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
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 { ZFullFieldSchema } from '@documenso/lib/types/field';
|
||||||
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
|
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
|
||||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
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 { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
|
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 { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
|
||||||
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
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 { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
|
||||||
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-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() {
|
export default function EnvelopeSignerPageRenderer() {
|
||||||
const { t, i18n } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
|
||||||
@ -91,6 +104,36 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
);
|
);
|
||||||
}, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]);
|
}, [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 }) => {
|
const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
|
||||||
if (!pageLayer.current) {
|
if (!pageLayer.current) {
|
||||||
console.error('Layer not loaded yet');
|
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 (
|
const signField = async (
|
||||||
fieldId: number,
|
fieldId: number,
|
||||||
payload: TSignEnvelopeFieldValue,
|
payload: TSignEnvelopeFieldValue,
|
||||||
@ -412,11 +495,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
* Initialize the Konva page canvas and all fields and interactions.
|
* Initialize the Konva page canvas and all fields and interactions.
|
||||||
*/
|
*/
|
||||||
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||||
// Render the fields.
|
renderFields();
|
||||||
for (const field of localPageFields) {
|
|
||||||
renderFieldOnLayer(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPageLayer.batchDraw();
|
currentPageLayer.batchDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -428,9 +507,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
localPageFields.forEach((field) => {
|
renderFields();
|
||||||
renderFieldOnLayer(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
pageLayer.current.batchDraw();
|
||||||
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]);
|
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]);
|
||||||
@ -446,9 +523,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
// Rerender the whole page.
|
// Rerender the whole page.
|
||||||
pageLayer.current.destroyChildren();
|
pageLayer.current.destroyChildren();
|
||||||
|
|
||||||
localPageFields.forEach((field) => {
|
renderFields();
|
||||||
renderFieldOnLayer(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
pageLayer.current.batchDraw();
|
||||||
}, [selectedAssistantRecipient]);
|
}, [selectedAssistantRecipient]);
|
||||||
@ -475,6 +550,15 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
</EnvelopeFieldToolTip>
|
</EnvelopeFieldToolTip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{localPageOtherRecipientFields.map((field) => (
|
||||||
|
<EnvelopeRecipientFieldTooltip
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
showFieldStatus={true}
|
||||||
|
showRecipientTooltip={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* The element Konva will inject it's canvas into. */}
|
{/* The element Konva will inject it's canvas into. */}
|
||||||
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
<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 { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||||
|
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||||
|
|
||||||
export type TemplatePageViewInformationProps = {
|
export type TemplatePageViewInformationProps = {
|
||||||
userId: number;
|
userId: number;
|
||||||
template: {
|
template: {
|
||||||
userId: number;
|
userId: number;
|
||||||
|
secondaryId: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
user: Pick<User, 'id' | 'name' | 'email'>;
|
user: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
@ -43,6 +45,10 @@ export const TemplatePageViewInformation = ({
|
|||||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
.toRelative(),
|
.toRelative(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: msg`Template ID (Legacy)`,
|
||||||
|
value: mapSecondaryIdToTemplateId(template.secondaryId),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isMounted, template, userId]);
|
}, [isMounted, template, userId]);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { SignFieldNumberDialog } from '~/components/dialogs/sign-field-number-di
|
|||||||
|
|
||||||
type HandleNumberFieldClickOptions = {
|
type HandleNumberFieldClickOptions = {
|
||||||
field: TFieldNumber;
|
field: TFieldNumber;
|
||||||
number: number | null;
|
number: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleNumberFieldClick = async (
|
export const handleNumberFieldClick = async (
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
"@simplewebauthn/server": "^9.0.3",
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
|
"content-disposition": "^0.5.4",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"hono": "4.7.0",
|
"hono": "4.7.0",
|
||||||
"hono-rate-limiter": "^0.4.2",
|
"hono-rate-limiter": "^0.4.2",
|
||||||
@ -87,6 +88,7 @@
|
|||||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
"@simplewebauthn/types": "^9.0.1",
|
"@simplewebauthn/types": "^9.0.1",
|
||||||
|
"@types/content-disposition": "^0.5.9",
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@ -104,5 +106,5 @@
|
|||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "2.0.0"
|
"version": "2.0.6"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { type DocumentDataType, DocumentStatus } from '@prisma/client';
|
import { type DocumentDataType, DocumentStatus } from '@prisma/client';
|
||||||
|
import contentDisposition from 'content-disposition';
|
||||||
import { type Context } from 'hono';
|
import { type Context } from 'hono';
|
||||||
|
|
||||||
import { sha256 } from '@documenso/lib/universal/crypto';
|
import { sha256 } from '@documenso/lib/universal/crypto';
|
||||||
@ -34,7 +35,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
|||||||
|
|
||||||
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
|
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
|
||||||
|
|
||||||
if (c.req.header('If-None-Match') === etag) {
|
if (c.req.header('If-None-Match') === etag && !isDownload) {
|
||||||
return c.body(null, 304);
|
return c.body(null, 304);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +59,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
|||||||
if (status === DocumentStatus.COMPLETED) {
|
if (status === DocumentStatus.COMPLETED) {
|
||||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
} else {
|
} else {
|
||||||
// Set a tiny 1 minute cache, with must-revalidate to ensure the client always checks for updates.
|
c.header('Cache-Control', 'public, max-age=0, must-revalidate');
|
||||||
c.header('Cache-Control', 'public, max-age=60, must-revalidate');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
|||||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||||
const filename = `${baseTitle}${suffix}`;
|
const filename = `${baseTitle}${suffix}`;
|
||||||
|
|
||||||
c.header('Content-Disposition', `attachment; filename="${filename}"`);
|
c.header('Content-Disposition', contentDisposition(filename));
|
||||||
|
|
||||||
// For downloads, prevent caching to ensure fresh data
|
// For downloads, prevent caching to ensure fresh data
|
||||||
c.header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
c.header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
|||||||
15
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.0.0",
|
"version": "2.0.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.0.0",
|
"version": "2.0.6",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@ -100,7 +100,7 @@
|
|||||||
},
|
},
|
||||||
"apps/remix": {
|
"apps/remix": {
|
||||||
"name": "@documenso/remix",
|
"name": "@documenso/remix",
|
||||||
"version": "2.0.0",
|
"version": "2.0.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cantoo/pdf-lib": "^2.5.2",
|
"@cantoo/pdf-lib": "^2.5.2",
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@ -129,6 +129,7 @@
|
|||||||
"@simplewebauthn/server": "^9.0.3",
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
|
"content-disposition": "^0.5.4",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"hono": "4.7.0",
|
"hono": "4.7.0",
|
||||||
"hono-rate-limiter": "^0.4.2",
|
"hono-rate-limiter": "^0.4.2",
|
||||||
@ -175,6 +176,7 @@
|
|||||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
"@simplewebauthn/types": "^9.0.1",
|
"@simplewebauthn/types": "^9.0.1",
|
||||||
|
"@types/content-disposition": "^0.5.9",
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@ -12315,6 +12317,13 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/content-disposition": {
|
||||||
|
"version": "0.5.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz",
|
||||||
|
"integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/cross-spawn": {
|
"node_modules/@types/cross-spawn": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0",
|
"version": "2.0.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev --filter=@documenso/remix",
|
"dev": "turbo run dev --filter=@documenso/remix",
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { FieldType } from '@prisma/client';
|
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 type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
|
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
|
||||||
@ -13,11 +15,66 @@ export type FieldTestData = TFieldAndMeta & {
|
|||||||
signature?: string;
|
signature?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const columnWidth = 19.125;
|
export const signatureBase64Demo = `data:image/png;base64,${fs.readFileSync(
|
||||||
const rowHeight = 6.7;
|
path.join(__dirname, '../../../packages/assets/', 'logo_icon.png'),
|
||||||
|
'base64',
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
const columnWidth = 19.125;
|
||||||
|
const fullColumnWidth = 57.37499999999998;
|
||||||
|
const rowHeight = 6.7;
|
||||||
|
const rowPadding = 0;
|
||||||
|
|
||||||
|
const calculatePositionPageOne = (
|
||||||
|
row: number,
|
||||||
|
column: number,
|
||||||
|
width: 'full' | 'column' = 'column',
|
||||||
|
) => {
|
||||||
const alignmentGridStartX = 31;
|
const alignmentGridStartX = 31;
|
||||||
const alignmentGridStartY = 19.02;
|
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[] = [
|
export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||||
/**
|
/**
|
||||||
@ -31,10 +88,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'email',
|
type: 'email',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(0, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'admin@documenso.com',
|
customText: 'admin@documenso.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -44,10 +98,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'email',
|
type: 'email',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(0, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'admin@documenso.com',
|
customText: 'admin@documenso.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -58,10 +109,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'email',
|
type: 'email',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(0, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'admin@documenso.com',
|
customText: 'admin@documenso.com',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -75,10 +123,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'name',
|
type: 'name',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(1, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'John Doe',
|
customText: 'John Doe',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -88,10 +133,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'name',
|
type: 'name',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(1, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'John Doe',
|
customText: 'John Doe',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -102,10 +144,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'name',
|
type: 'name',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(1, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'John Doe',
|
customText: 'John Doe',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -119,10 +158,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(2, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -132,10 +168,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(2, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -146,10 +179,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(2, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -163,10 +193,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(3, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -176,10 +203,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(3, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -190,10 +214,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(3, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -207,10 +228,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(4, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -220,10 +238,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(4, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -234,10 +249,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(4, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -251,10 +263,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'initials',
|
type: 'initials',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(5, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'JD',
|
customText: 'JD',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -264,10 +273,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'initials',
|
type: 'initials',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(5, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'JD',
|
customText: 'JD',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -278,10 +284,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'initials',
|
type: 'initials',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(5, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'JD',
|
customText: 'JD',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -299,10 +302,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(6, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '0',
|
customText: '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -312,15 +312,12 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'radio',
|
type: 'radio',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: true, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(6, 1),
|
||||||
width: columnWidth,
|
customText: '',
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '2',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.RADIO,
|
type: FieldType.RADIO,
|
||||||
@ -330,15 +327,12 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'radio',
|
type: 'radio',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: true, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(6, 2),
|
||||||
width: columnWidth,
|
customText: '1',
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Row 8 Checkbox
|
* Row 8 Checkbox
|
||||||
@ -355,10 +349,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(7, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: toCheckboxCustomText([0]),
|
customText: toCheckboxCustomText([0]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -368,15 +359,12 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: true, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(7, 1),
|
||||||
width: columnWidth,
|
customText: '',
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: toCheckboxCustomText([1]),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.CHECKBOX,
|
type: FieldType.CHECKBOX,
|
||||||
@ -386,15 +374,12 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: true, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(7, 2),
|
||||||
width: columnWidth,
|
customText: toCheckboxCustomText([1]),
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Row 8 Dropdown
|
* Row 8 Dropdown
|
||||||
@ -407,10 +392,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(8, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'Option 1',
|
customText: 'Option 1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -420,10 +402,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(8, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'Option 1',
|
customText: 'Option 1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -434,10 +413,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(8, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'Option 1',
|
customText: 'Option 1',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -450,10 +426,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'signature',
|
type: 'signature',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(9, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: 'My Signature',
|
||||||
},
|
},
|
||||||
@ -463,10 +436,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'signature',
|
type: 'signature',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(9, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: 'My Signature',
|
||||||
},
|
},
|
||||||
@ -477,22 +447,295 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'signature',
|
type: 'signature',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(9, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
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;
|
] 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';
|
} from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
||||||
|
|
||||||
import type { FieldTestData } from './field-alignment-pdf';
|
import type { FieldTestData } from './field-alignment-pdf';
|
||||||
|
import { signatureBase64Demo } from './field-alignment-pdf';
|
||||||
|
|
||||||
const columnWidth = 20.1;
|
const columnWidth = 20.1;
|
||||||
const fullColumnWidth = 75.8;
|
const fullColumnWidth = 75.8;
|
||||||
@ -37,7 +38,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
page: 2,
|
page: 2,
|
||||||
...calculatePosition(0, 0),
|
...calculatePosition(0, 0),
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: signatureBase64Demo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.SIGNATURE,
|
type: FieldType.SIGNATURE,
|
||||||
@ -47,7 +48,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
page: 2,
|
page: 2,
|
||||||
...calculatePosition(1, 0),
|
...calculatePosition(1, 0),
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: signatureBase64Demo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.SIGNATURE,
|
type: FieldType.SIGNATURE,
|
||||||
@ -67,7 +68,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
page: 2,
|
page: 2,
|
||||||
...calculatePosition(3, 0),
|
...calculatePosition(3, 0),
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: 'My Signature super overflow maybe',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,7 +81,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(0, 0, 'full'),
|
...calculatePosition(0, 0, 'full'),
|
||||||
customText: '123456789',
|
customText: 'Hello world, this is some random text that I have written here',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -89,7 +90,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(1, 0),
|
...calculatePosition(1, 0),
|
||||||
customText: '123456789123456789123456789123456789',
|
customText: 'Some text that should overflow correctly',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -109,7 +110,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(3, 0),
|
...calculatePosition(3, 0),
|
||||||
customText: '123456789',
|
customText: 'Input should have a placeholder text when clicked',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -119,7 +120,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(3, 1),
|
...calculatePosition(3, 1),
|
||||||
customText: '123456789',
|
customText: 'Should have a label during editing and signing',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -129,7 +130,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(3, 2),
|
...calculatePosition(3, 2),
|
||||||
customText: '123456789',
|
customText: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -139,20 +140,19 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(4, 0),
|
...calculatePosition(4, 0),
|
||||||
customText: '123456789',
|
customText: 'This is a required field',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
text: 'Readonly Value',
|
text: 'Some Readonly Value',
|
||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(4, 1),
|
...calculatePosition(4, 1),
|
||||||
customText: 'Readonly Value',
|
customText: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PAGE 4 NUMBER
|
* PAGE 4 NUMBER
|
||||||
*/
|
*/
|
||||||
@ -220,7 +220,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
value: '123',
|
value: '123456789',
|
||||||
},
|
},
|
||||||
page: 4,
|
page: 4,
|
||||||
...calculatePosition(3, 2),
|
...calculatePosition(3, 2),
|
||||||
@ -241,10 +241,11 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
value: '123456789',
|
||||||
},
|
},
|
||||||
page: 4,
|
page: 4,
|
||||||
...calculatePosition(4, 1),
|
...calculatePosition(4, 1),
|
||||||
customText: '123456789',
|
customText: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -272,8 +273,8 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'radio',
|
type: 'radio',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: true, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
{ id: 3, checked: false, value: 'Option 3' },
|
{ id: 3, checked: true, value: 'Option 3' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 5,
|
page: 5,
|
||||||
@ -285,6 +286,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
direction: 'vertical',
|
direction: 'vertical',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
required: true,
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
@ -293,17 +295,18 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 5,
|
page: 5,
|
||||||
...calculatePosition(2, 0),
|
...calculatePosition(2, 0),
|
||||||
customText: '',
|
customText: '2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.RADIO,
|
type: FieldType.RADIO,
|
||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
direction: 'vertical',
|
direction: 'vertical',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
readOnly: true,
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
{ id: 3, checked: false, value: 'Option 3' },
|
{ id: 3, checked: true, value: 'Option 3' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 5,
|
page: 5,
|
||||||
@ -338,7 +341,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: true, value: 'Option 2' },
|
{ id: 2, checked: true, value: 'Option 2' },
|
||||||
{ id: 2, checked: true, value: 'Option 3' },
|
{ id: 3, checked: false, value: 'Option 3' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 6,
|
page: 6,
|
||||||
@ -358,7 +361,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 6,
|
page: 6,
|
||||||
...calculatePosition(2, 0),
|
...calculatePosition(2, 0),
|
||||||
customText: '',
|
customText: toCheckboxCustomText([2]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.CHECKBOX,
|
type: FieldType.CHECKBOX,
|
||||||
@ -368,7 +371,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: true, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 6,
|
page: 6,
|
||||||
@ -445,11 +448,11 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
|
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
|
||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
defaultValue: 'Option 1',
|
defaultValue: 'Option 2',
|
||||||
},
|
},
|
||||||
page: 7,
|
page: 7,
|
||||||
...calculatePosition(1, 0),
|
...calculatePosition(1, 0),
|
||||||
customText: 'Option 1',
|
customText: 'Option 2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.DROPDOWN,
|
type: FieldType.DROPDOWN,
|
||||||
@ -460,13 +463,14 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 7,
|
page: 7,
|
||||||
...calculatePosition(2, 0),
|
...calculatePosition(2, 0),
|
||||||
customText: 'Option 1',
|
customText: 'Option 3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.DROPDOWN,
|
type: FieldType.DROPDOWN,
|
||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
|
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
|
||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
|
defaultValue: 'Option 1',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
page: 7,
|
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 { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||||
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-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';
|
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';
|
||||||
|
|
||||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
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)
|
// Step 6: Create fields for first PDF (alignment fields)
|
||||||
const alignmentFieldsRequest = {
|
const alignmentFieldsRequest = {
|
||||||
envelopeId: createdEnvelope.id,
|
envelopeId: createdEnvelope.id,
|
||||||
data: formatAlignmentTestFields.map((field) => ({
|
data: ALIGNMENT_TEST_FIELDS.map((field) => ({
|
||||||
recipientId,
|
recipientId,
|
||||||
envelopeItemId: alignmentItem.id,
|
envelopeItemId: alignmentItem.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
@ -547,7 +547,7 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
expect(finalEnvelope.envelopeItems.length).toBe(2);
|
expect(finalEnvelope.envelopeItems.length).toBe(2);
|
||||||
expect(finalEnvelope.recipients.length).toBe(1);
|
expect(finalEnvelope.recipients.length).toBe(1);
|
||||||
expect(finalEnvelope.fields.length).toBe(
|
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.title).toBe('Envelope Full Field Test');
|
||||||
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
|
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
|
||||||
|
|||||||
@ -21,34 +21,226 @@ import pixelMatch from 'pixelmatch';
|
|||||||
import { PNG } from 'pngjs';
|
import { PNG } from 'pngjs';
|
||||||
import type { TestInfo } from '@playwright/test';
|
import type { TestInfo } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
||||||
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
|
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
|
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import type {
|
||||||
|
TCreateEnvelopePayload,
|
||||||
|
TCreateEnvelopeResponse,
|
||||||
|
} from '../../../trpc/server/envelope-router/create-envelope.types';
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
|
||||||
|
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
|
||||||
|
import { RecipientRole } from '../../../prisma/generated/types';
|
||||||
|
import { FIELD_META_TEST_FIELDS } from '../../constants/field-meta-pdf';
|
||||||
|
import { ALIGNMENT_TEST_FIELDS } from '../../constants/field-alignment-pdf';
|
||||||
|
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
|
||||||
|
import { isBase64Image } from '../../../lib/constants/signatures';
|
||||||
|
|
||||||
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||||
|
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
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, team } = await seedUser();
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
const envelope = await seedAlignmentTestDocument({
|
email: 'example@documenso.com',
|
||||||
userId: user.id,
|
},
|
||||||
teamId: team.id,
|
include: {
|
||||||
recipientName: user.name || '',
|
ownedOrganisations: {
|
||||||
recipientEmail: user.email,
|
include: {
|
||||||
insertFields: true,
|
teams: true,
|
||||||
status: DocumentStatus.PENDING,
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = envelope.recipients[0].token;
|
const userId = user.id;
|
||||||
|
const teamId = user.ownedOrganisations[0].teams[0].id;
|
||||||
|
|
||||||
const signUrl = `/sign/${token}`;
|
await seedAlignmentTestDocument({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
recipientName: user.name || '',
|
||||||
|
recipientEmail: user.email,
|
||||||
|
insertFields: false,
|
||||||
|
status: DocumentStatus.DRAFT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('field placement visual regression', async ({ page, request }, testInfo) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const { token } = await createApiToken({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
tokenName: 'test',
|
||||||
|
expiresIn: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 1: Create initial envelope with Prisma (with first envelope item)
|
||||||
|
const alignmentPdf = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../../../assets/field-font-alignment.pdf'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldMetaPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/field-meta.pdf'));
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
const fieldMetaFields = FIELD_META_TEST_FIELDS.map((field) => ({
|
||||||
|
identifier: 'field-meta',
|
||||||
|
type: field.type,
|
||||||
|
page: field.page,
|
||||||
|
positionX: field.positionX,
|
||||||
|
positionY: field.positionY,
|
||||||
|
width: field.width,
|
||||||
|
height: field.height,
|
||||||
|
fieldMeta: field.fieldMeta,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const alignmentFields = ALIGNMENT_TEST_FIELDS.map((field) => ({
|
||||||
|
identifier: 'alignment-pdf',
|
||||||
|
type: field.type,
|
||||||
|
page: field.page,
|
||||||
|
positionX: field.positionX,
|
||||||
|
positionY: field.positionY,
|
||||||
|
width: field.width,
|
||||||
|
height: field.height,
|
||||||
|
fieldMeta: field.fieldMeta,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const createEnvelopePayload: TCreateEnvelopePayload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Envelope Full Field Test',
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
email: user.email,
|
||||||
|
name: user.name || '',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
fields: [...fieldMetaFields, ...alignmentFields],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
formData.append('payload', JSON.stringify(createEnvelopePayload));
|
||||||
|
|
||||||
|
formData.append('files', new File([alignmentPdf], 'alignment-pdf', { type: 'application/pdf' }));
|
||||||
|
formData.append('files', new File([fieldMetaPdf], 'field-meta', { type: 'application/pdf' }));
|
||||||
|
|
||||||
|
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createEnvelopeRequest.ok()).toBeTruthy();
|
||||||
|
expect(createEnvelopeRequest.status()).toBe(200);
|
||||||
|
|
||||||
|
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
|
||||||
|
|
||||||
|
const envelope = await prisma.envelope.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: createdEnvelopeId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
envelopeItems: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipientId = envelope.recipients[0].id;
|
||||||
|
const alignmentItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 1);
|
||||||
|
const fieldMetaItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 2);
|
||||||
|
|
||||||
|
expect(recipientId).toBeDefined();
|
||||||
|
expect(alignmentItem).toBeDefined();
|
||||||
|
expect(fieldMetaItem).toBeDefined();
|
||||||
|
|
||||||
|
if (!alignmentItem || !fieldMetaItem) {
|
||||||
|
throw new Error('Envelope items not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
} satisfies TDistributeEnvelopeRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
const uninsertedFields = await prisma.field.findMany({
|
||||||
|
where: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
inserted: false,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
envelopeItem: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
uninsertedFields.map(async (field) => {
|
||||||
|
let foundField = ALIGNMENT_TEST_FIELDS.find(
|
||||||
|
(f) =>
|
||||||
|
field.page === f.page &&
|
||||||
|
field.envelopeItem.title === 'alignment-pdf' &&
|
||||||
|
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||||
|
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||||
|
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||||
|
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!foundField) {
|
||||||
|
foundField = FIELD_META_TEST_FIELDS.find(
|
||||||
|
(f) =>
|
||||||
|
field.page === f.page &&
|
||||||
|
field.envelopeItem.title === 'field-meta' &&
|
||||||
|
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||||
|
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||||
|
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||||
|
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundField) {
|
||||||
|
throw new Error('Field not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.field.update({
|
||||||
|
where: {
|
||||||
|
id: field.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
inserted: true,
|
||||||
|
customText: foundField.customText,
|
||||||
|
signature: foundField.signature
|
||||||
|
? {
|
||||||
|
create: {
|
||||||
|
recipientId: envelope.recipients[0].id,
|
||||||
|
signatureImageAsBase64: isBase64Image(foundField.signature)
|
||||||
|
? foundField.signature
|
||||||
|
: null,
|
||||||
|
typedSignature: isBase64Image(foundField.signature) ? null : foundField.signature,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const recipientToken = envelope.recipients[0].token;
|
||||||
|
const signUrl = `/sign/${recipientToken}`;
|
||||||
|
|
||||||
await apiSignin({
|
await apiSignin({
|
||||||
page,
|
page,
|
||||||
@ -94,9 +286,10 @@ test.skip('field placement visual regression', async ({ page }, testInfo) => {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
completedDocument.envelopeItems.map(async (item) => {
|
completedDocument.envelopeItems.map(async (item) => {
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: item,
|
envelopeItem: item,
|
||||||
token,
|
token: recipientToken,
|
||||||
version: 'signed',
|
version: 'signed',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -179,7 +372,8 @@ test.skip('download envelope images', async ({ page }) => {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
completedDocument.envelopeItems.map(async (item) => {
|
completedDocument.envelopeItems.map(async (item) => {
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: item,
|
envelopeItem: item,
|
||||||
token,
|
token,
|
||||||
version: 'signed',
|
version: 'signed',
|
||||||
@ -287,7 +481,7 @@ const compareSignedPdfWithImages = async ({
|
|||||||
// Expect the certificate to NOT be blank. Since the storedImage is blank.
|
// Expect the certificate to NOT be blank. Since the storedImage is blank.
|
||||||
expect.soft(comparison).toBeGreaterThan(20000);
|
expect.soft(comparison).toBeGreaterThan(20000);
|
||||||
} else {
|
} else {
|
||||||
expect.soft(comparison).toEqual(0);
|
expect.soft(comparison).toBeLessThan(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
|
|||||||
import { DocumentStatus, FieldType } from '@prisma/client';
|
import { DocumentStatus, FieldType } from '@prisma/client';
|
||||||
|
|
||||||
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
|
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||||
@ -34,7 +34,8 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: data,
|
envelopeItem: data,
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
version: 'signed',
|
version: 'signed',
|
||||||
@ -85,7 +86,8 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
|
|
||||||
const firstDocumentData = completedDocument.envelopeItems[0];
|
const firstDocumentData = completedDocument.envelopeItems[0];
|
||||||
|
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: firstDocumentData,
|
envelopeItem: firstDocumentData,
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
version: 'signed',
|
version: 'signed',
|
||||||
@ -139,7 +141,8 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: data,
|
envelopeItem: data,
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
version: 'signed',
|
version: 'signed',
|
||||||
@ -188,7 +191,8 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
|
|
||||||
const firstDocumentData = completedDocument.envelopeItems[0];
|
const firstDocumentData = completedDocument.envelopeItems[0];
|
||||||
|
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: firstDocumentData,
|
envelopeItem: firstDocumentData,
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
version: 'signed',
|
version: 'signed',
|
||||||
@ -242,7 +246,8 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: data,
|
envelopeItem: data,
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
version: 'signed',
|
version: 'signed',
|
||||||
@ -289,7 +294,8 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: completedDocument.envelopeItems[0],
|
envelopeItem: completedDocument.envelopeItems[0],
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
version: 'signed',
|
version: 'signed',
|
||||||
|
|||||||
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 || {};
|
const { minValue, maxValue, readOnly, required, numberFormat, fontSize } = fieldMeta || {};
|
||||||
|
|
||||||
if (numberFormat) {
|
if (numberFormat && value.length > 0) {
|
||||||
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
|
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
|
||||||
|
|
||||||
if (!foundRegex) {
|
if (!foundRegex) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { EnvelopeItem } from '@prisma/client';
|
import type { EnvelopeItem } from '@prisma/client';
|
||||||
|
|
||||||
import { getEnvelopeDownloadUrl } from '../utils/envelope-download';
|
import { getEnvelopeItemPdfUrl } from '../utils/envelope-download';
|
||||||
import { downloadFile } from './download-file';
|
import { downloadFile } from './download-file';
|
||||||
|
|
||||||
type DocumentVersion = 'original' | 'signed';
|
type DocumentVersion = 'original' | 'signed';
|
||||||
@ -24,7 +24,8 @@ export const downloadPDF = async ({
|
|||||||
fileName,
|
fileName,
|
||||||
version = 'signed',
|
version = 'signed',
|
||||||
}: DownloadPDFProps) => {
|
}: DownloadPDFProps) => {
|
||||||
const downloadUrl = getEnvelopeDownloadUrl({
|
const downloadUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
envelopeItem: envelopeItem,
|
envelopeItem: envelopeItem,
|
||||||
token,
|
token,
|
||||||
version,
|
version,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
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 { FieldType } from '@prisma/client';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@ -63,6 +63,8 @@ type UseEditorFieldsResponse = {
|
|||||||
// Selected recipient
|
// Selected recipient
|
||||||
selectedRecipient: Recipient | null;
|
selectedRecipient: Recipient | null;
|
||||||
setSelectedRecipient: (recipientId: number | null) => void;
|
setSelectedRecipient: (recipientId: number | null) => void;
|
||||||
|
|
||||||
|
resetForm: (fields?: Field[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useEditorFields = ({
|
export const useEditorFields = ({
|
||||||
@ -72,9 +74,8 @@ export const useEditorFields = ({
|
|||||||
const [selectedFieldFormId, setSelectedFieldFormId] = useState<string | null>(null);
|
const [selectedFieldFormId, setSelectedFieldFormId] = useState<string | null>(null);
|
||||||
const [selectedRecipientId, setSelectedRecipientId] = useState<number | null>(null);
|
const [selectedRecipientId, setSelectedRecipientId] = useState<number | null>(null);
|
||||||
|
|
||||||
const form = useForm<TEditorFieldsFormSchema>({
|
const generateDefaultValues = (fields?: Field[]) => {
|
||||||
defaultValues: {
|
const formFields = (fields || envelope.fields).map(
|
||||||
fields: envelope.fields.map(
|
|
||||||
(field): TLocalField => ({
|
(field): TLocalField => ({
|
||||||
id: field.id,
|
id: field.id,
|
||||||
formId: nanoid(),
|
formId: nanoid(),
|
||||||
@ -88,8 +89,15 @@ export const useEditorFields = ({
|
|||||||
recipientId: field.recipientId,
|
recipientId: field.recipientId,
|
||||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||||
}),
|
}),
|
||||||
),
|
);
|
||||||
},
|
|
||||||
|
return {
|
||||||
|
fields: formFields,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm<TEditorFieldsFormSchema>({
|
||||||
|
defaultValues: generateDefaultValues(),
|
||||||
resolver: zodResolver(ZEditorFieldsFormSchema),
|
resolver: zodResolver(ZEditorFieldsFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -272,6 +280,10 @@ export const useEditorFields = ({
|
|||||||
setSelectedRecipientId(foundRecipient?.id ?? null);
|
setSelectedRecipientId(foundRecipient?.id ?? null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetForm = (fields?: Field[]) => {
|
||||||
|
form.reset(generateDefaultValues(fields));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Core state
|
// Core state
|
||||||
localFields,
|
localFields,
|
||||||
@ -295,6 +307,8 @@ export const useEditorFields = ({
|
|||||||
// Selected recipient
|
// Selected recipient
|
||||||
selectedRecipient,
|
selectedRecipient,
|
||||||
setSelectedRecipient,
|
setSelectedRecipient,
|
||||||
|
|
||||||
|
resetForm,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,10 @@ export function usePageRenderer(renderFunction: RenderFunction) {
|
|||||||
stage: stage.current,
|
stage: stage.current,
|
||||||
pageLayer: pageLayer.current,
|
pageLayer: pageLayer.current,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void document.fonts.ready.then(function () {
|
||||||
|
pageLayer.current?.batchDraw();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
|
|||||||
|
|
||||||
import type { TEnvelope } from '../../types/envelope';
|
import type { TEnvelope } from '../../types/envelope';
|
||||||
import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
|
import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
|
||||||
import { getEnvelopeDownloadUrl } from '../../utils/envelope-download';
|
import { getEnvelopeItemPdfUrl } from '../../utils/envelope-download';
|
||||||
|
|
||||||
type FileData =
|
type FileData =
|
||||||
| {
|
| {
|
||||||
@ -30,6 +30,8 @@ type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
|
|||||||
type EnvelopeRenderProviderValue = {
|
type EnvelopeRenderProviderValue = {
|
||||||
getPdfBuffer: (envelopeItemId: string) => FileData | null;
|
getPdfBuffer: (envelopeItemId: string) => FileData | null;
|
||||||
envelopeItems: EnvelopeRenderItem[];
|
envelopeItems: EnvelopeRenderItem[];
|
||||||
|
envelopeStatus: TEnvelope['status'];
|
||||||
|
envelopeType: TEnvelope['type'];
|
||||||
currentEnvelopeItem: EnvelopeRenderItem | null;
|
currentEnvelopeItem: EnvelopeRenderItem | null;
|
||||||
setCurrentEnvelopeItem: (envelopeItemId: string) => void;
|
setCurrentEnvelopeItem: (envelopeItemId: string) => void;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
@ -44,7 +46,7 @@ type EnvelopeRenderProviderValue = {
|
|||||||
interface EnvelopeRenderProviderProps {
|
interface EnvelopeRenderProviderProps {
|
||||||
children: React.ReactNode;
|
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.
|
* Optional fields which are passed down to renderers for custom rendering needs.
|
||||||
@ -100,7 +102,7 @@ export const EnvelopeRenderProvider = ({
|
|||||||
// Indexed by documentDataId.
|
// Indexed by documentDataId.
|
||||||
const [files, setFiles] = useState<Record<string, FileData>>({});
|
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);
|
const [renderError, setRenderError] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -124,10 +126,10 @@ export const EnvelopeRenderProvider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const downloadUrl = getEnvelopeDownloadUrl({
|
const downloadUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'view',
|
||||||
envelopeItem: envelopeItem,
|
envelopeItem: envelopeItem,
|
||||||
token,
|
token,
|
||||||
version: 'signed',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
|
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
|
||||||
@ -163,11 +165,15 @@ export const EnvelopeRenderProvider = ({
|
|||||||
const setCurrentEnvelopeItem = (envelopeItemId: string) => {
|
const setCurrentEnvelopeItem = (envelopeItemId: string) => {
|
||||||
const foundItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId);
|
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.
|
// Set the selected item to the first item if none is set.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (currentItem && !envelopeItems.some((item) => item.id === currentItem.id)) {
|
||||||
|
setCurrentItem(null);
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentItem && envelopeItems.length > 0) {
|
if (!currentItem && envelopeItems.length > 0) {
|
||||||
setCurrentEnvelopeItem(envelopeItems[0].id);
|
setCurrentEnvelopeItem(envelopeItems[0].id);
|
||||||
}
|
}
|
||||||
@ -203,6 +209,8 @@ export const EnvelopeRenderProvider = ({
|
|||||||
value={{
|
value={{
|
||||||
getPdfBuffer,
|
getPdfBuffer,
|
||||||
envelopeItems,
|
envelopeItems,
|
||||||
|
envelopeStatus: envelope.status,
|
||||||
|
envelopeType: envelope.type,
|
||||||
currentEnvelopeItem: currentItem,
|
currentEnvelopeItem: currentItem,
|
||||||
setCurrentEnvelopeItem,
|
setCurrentEnvelopeItem,
|
||||||
fields: fields ?? [],
|
fields: fields ?? [],
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export type JobDefinition<Name extends string = string, Schema = any> = {
|
|||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
optimizeParallelism?: boolean;
|
||||||
trigger: {
|
trigger: {
|
||||||
name: Name;
|
name: Name;
|
||||||
schema?: z.ZodType<Schema>;
|
schema?: z.ZodType<Schema>;
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export class InngestJobProvider extends BaseJobProvider {
|
|||||||
{
|
{
|
||||||
id: job.id,
|
id: job.id,
|
||||||
name: job.name,
|
name: job.name,
|
||||||
|
optimizeParallelism: job.optimizeParallelism ?? false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
event: job.trigger.name,
|
event: job.trigger.name,
|
||||||
|
|||||||
@ -189,9 +189,44 @@ export const run = async ({
|
|||||||
settings,
|
settings,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newDocumentData = await Promise.all(
|
// !: The commented out code is our desired implementation but we're seemingly
|
||||||
envelopeItems.map(async (envelopeItem) =>
|
// !: running into issues with inngest parallelism in production.
|
||||||
io.runTask(`decorate-and-sign-envelope-item-${envelopeItem.id}`, async () => {
|
// !: Until this is resolved we will do this sequentially which is slower but
|
||||||
|
// !: will actually work.
|
||||||
|
// const decoratePromises: Array<Promise<{ oldDocumentDataId: string; newDocumentDataId: string }>> =
|
||||||
|
// [];
|
||||||
|
|
||||||
|
// for (const envelopeItem of envelopeItems) {
|
||||||
|
// const task = io.runTask(`decorate-${envelopeItem.id}`, async () => {
|
||||||
|
// const envelopeItemFields = envelope.envelopeItems.find(
|
||||||
|
// (item) => item.id === envelopeItem.id,
|
||||||
|
// )?.field;
|
||||||
|
|
||||||
|
// if (!envelopeItemFields) {
|
||||||
|
// throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return decorateAndSignPdf({
|
||||||
|
// envelope,
|
||||||
|
// envelopeItem,
|
||||||
|
// envelopeItemFields,
|
||||||
|
// isRejected,
|
||||||
|
// rejectionReason,
|
||||||
|
// certificateData,
|
||||||
|
// auditLogData,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// decoratePromises.push(task);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const newDocumentData = await Promise.all(decoratePromises);
|
||||||
|
|
||||||
|
// TODO: Remove once parallelization is working
|
||||||
|
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
|
||||||
|
|
||||||
|
for (const envelopeItem of envelopeItems) {
|
||||||
|
const result = await io.runTask(`decorate-${envelopeItem.id}`, async () => {
|
||||||
const envelopeItemFields = envelope.envelopeItems.find(
|
const envelopeItemFields = envelope.envelopeItems.find(
|
||||||
(item) => item.id === envelopeItem.id,
|
(item) => item.id === envelopeItem.id,
|
||||||
)?.field;
|
)?.field;
|
||||||
@ -209,9 +244,10 @@ export const run = async ({
|
|||||||
certificateData,
|
certificateData,
|
||||||
auditLogData,
|
auditLogData,
|
||||||
});
|
});
|
||||||
}),
|
});
|
||||||
),
|
|
||||||
);
|
newDocumentData.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
const postHog = PostHogServerClient();
|
const postHog = PostHogServerClient();
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
|
|||||||
id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
||||||
name: 'Seal Document',
|
name: 'Seal Document',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
|
optimizeParallelism: true,
|
||||||
trigger: {
|
trigger: {
|
||||||
name: SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
name: SEAL_DOCUMENT_JOB_DEFINITION_ID,
|
||||||
schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,
|
schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { DocumentData, Envelope, EnvelopeItem } from '@prisma/client';
|
import type { DocumentData, Envelope, EnvelopeItem, Field } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
@ -24,7 +24,9 @@ import {
|
|||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
ZDropdownFieldMeta,
|
ZDropdownFieldMeta,
|
||||||
ZFieldAndMetaSchema,
|
ZFieldAndMetaSchema,
|
||||||
|
ZNumberFieldMeta,
|
||||||
ZRadioFieldMeta,
|
ZRadioFieldMeta,
|
||||||
|
ZTextFieldMeta,
|
||||||
} from '../../types/field-meta';
|
} from '../../types/field-meta';
|
||||||
import {
|
import {
|
||||||
ZWebhookDocumentSchema,
|
ZWebhookDocumentSchema,
|
||||||
@ -182,80 +184,19 @@ export const sendDocument = async ({
|
|||||||
// Validate and autoinsert fields for V2 envelopes.
|
// Validate and autoinsert fields for V2 envelopes.
|
||||||
if (envelope.internalVersion === 2) {
|
if (envelope.internalVersion === 2) {
|
||||||
for (const unknownField of envelope.fields) {
|
for (const unknownField of envelope.fields) {
|
||||||
const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
|
const recipient = envelope.recipients.find((r) => r.id === unknownField.recipientId);
|
||||||
|
|
||||||
if (parsedField.error) {
|
if (!recipient) {
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
message: 'One or more fields have invalid metadata. Error: ' + parsedField.error.message,
|
message: 'Recipient not found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const field = parsedField.data;
|
const fieldToAutoInsert = extractFieldAutoInsertValues(unknownField);
|
||||||
const fieldId = unknownField.id;
|
|
||||||
|
|
||||||
if (field.type === FieldType.RADIO) {
|
// Only auto-insert fields if the recipient has not been sent the document yet.
|
||||||
const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
|
if (fieldToAutoInsert && recipient.sendStatus !== SendStatus.SENT) {
|
||||||
|
fieldsToAutoInsert.push(fieldToAutoInsert);
|
||||||
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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,6 +216,7 @@ export const sendDocument = async ({
|
|||||||
if (envelope.internalVersion === 2) {
|
if (envelope.internalVersion === 2) {
|
||||||
const autoInsertedFields = await Promise.all(
|
const autoInsertedFields = await Promise.all(
|
||||||
fieldsToAutoInsert.map(async (field) => {
|
fieldsToAutoInsert.map(async (field) => {
|
||||||
|
// Warning: Only auto-insert fields if the recipient has not been sent the document yet.
|
||||||
return await tx.field.update({
|
return await tx.field.update({
|
||||||
where: {
|
where: {
|
||||||
id: field.fieldId,
|
id: field.fieldId,
|
||||||
@ -387,3 +329,113 @@ const injectFormValuesIntoDocument = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the auto insertion values for a given field.
|
||||||
|
*
|
||||||
|
* If field is not auto insertable, returns `null`.
|
||||||
|
*/
|
||||||
|
export const extractFieldAutoInsertValues = (
|
||||||
|
unknownField: Field,
|
||||||
|
): { fieldId: number; customText: string } | null => {
|
||||||
|
const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
|
||||||
|
|
||||||
|
if (parsedField.error) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: 'One or more fields have invalid metadata. Error: ' + parsedField.error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = parsedField.data;
|
||||||
|
const fieldId = unknownField.id;
|
||||||
|
|
||||||
|
// Auto insert text fields with prefilled values.
|
||||||
|
if (field.type === FieldType.TEXT) {
|
||||||
|
const { text } = ZTextFieldMeta.parse(field.fieldMeta);
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
return {
|
||||||
|
fieldId,
|
||||||
|
customText: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto insert number fields with prefilled values.
|
||||||
|
if (field.type === FieldType.NUMBER) {
|
||||||
|
const { value } = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
return {
|
||||||
|
fieldId,
|
||||||
|
customText: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto insert radio fields with the pre-checked value.
|
||||||
|
if (field.type === FieldType.RADIO) {
|
||||||
|
const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||||
|
|
||||||
|
const checkedItemIndex = values.findIndex((value) => value.checked);
|
||||||
|
|
||||||
|
if (checkedItemIndex !== -1) {
|
||||||
|
return {
|
||||||
|
fieldId,
|
||||||
|
customText: toRadioCustomText(checkedItemIndex),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto insert dropdown fields with the default value.
|
||||||
|
if (field.type === FieldType.DROPDOWN) {
|
||||||
|
const { defaultValue, values = [] } = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||||
|
|
||||||
|
if (defaultValue && values.some((value) => value.value === defaultValue)) {
|
||||||
|
return {
|
||||||
|
fieldId,
|
||||||
|
customText: defaultValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto insert checkbox fields with the pre-checked values.
|
||||||
|
if (field.type === FieldType.CHECKBOX) {
|
||||||
|
const {
|
||||||
|
values = [],
|
||||||
|
validationRule,
|
||||||
|
validationLength,
|
||||||
|
} = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||||
|
|
||||||
|
const checkedIndices: number[] = [];
|
||||||
|
|
||||||
|
values.forEach((value, i) => {
|
||||||
|
if (value.checked) {
|
||||||
|
checkedIndices.push(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
if (validationRule && validationLength) {
|
||||||
|
const validation = checkboxValidationSigns.find((sign) => sign.label === validationRule);
|
||||||
|
|
||||||
|
if (!validation) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: 'Invalid checkbox validation rule',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = validateCheckboxLength(checkedIndices.length, validation.value, validationLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid && checkedIndices.length > 0) {
|
||||||
|
return {
|
||||||
|
fieldId,
|
||||||
|
customText: toCheckboxCustomText(checkedIndices),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { prisma } from '@documenso/prisma';
|
|||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
|
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
|
import { extractFieldAutoInsertValues } from '../document/send-document';
|
||||||
import { getTeamSettings } from '../team/get-team-settings';
|
import { getTeamSettings } from '../team/get-team-settings';
|
||||||
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
||||||
import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
||||||
@ -144,6 +145,19 @@ export const getEnvelopeForDirectTemplateSigning = async ({
|
|||||||
recipient: {
|
recipient: {
|
||||||
...recipient,
|
...recipient,
|
||||||
directToken: envelope.directLink?.token || '',
|
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,
|
recipientSignature: null,
|
||||||
isRecipientsTurn: true,
|
isRecipientsTurn: true,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import type { TDocumentAuthMethods } from '../../types/document-auth';
|
import type { TDocumentAuthMethods } from '../../types/document-auth';
|
||||||
import { ZFieldSchema } from '../../types/field';
|
import { ZEnvelopeFieldSchema, ZFieldSchema } from '../../types/field';
|
||||||
import { ZRecipientLiteSchema } from '../../types/recipient';
|
import { ZRecipientLiteSchema } from '../../types/recipient';
|
||||||
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
|
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
|
||||||
import { getTeamSettings } from '../team/get-team-settings';
|
import { getTeamSettings } from '../team/get-team-settings';
|
||||||
@ -63,9 +63,11 @@ export const ZEnvelopeForSigningResponse = z.object({
|
|||||||
rejectionReason: true,
|
rejectionReason: true,
|
||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
fields: ZFieldSchema.omit({
|
fields: ZEnvelopeFieldSchema.extend({
|
||||||
documentId: true,
|
signature: SignatureSchema.pick({
|
||||||
templateId: true,
|
signatureImageAsBase64: true,
|
||||||
|
typedSignature: true,
|
||||||
|
}).nullish(),
|
||||||
}).array(),
|
}).array(),
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
|
|||||||
@ -129,7 +129,7 @@ export const setFieldsForTemplate = async ({
|
|||||||
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
||||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||||
const errors = validateNumberField(
|
const errors = validateNumberField(
|
||||||
String(numberFieldParsedMeta.value),
|
String(numberFieldParsedMeta.value || ''),
|
||||||
numberFieldParsedMeta,
|
numberFieldParsedMeta,
|
||||||
);
|
);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
|
|||||||
@ -88,11 +88,13 @@ export const addUserToOrganisation = async ({
|
|||||||
organisationId,
|
organisationId,
|
||||||
organisationGroups,
|
organisationGroups,
|
||||||
organisationMemberRole,
|
organisationMemberRole,
|
||||||
|
bypassEmail = false,
|
||||||
}: {
|
}: {
|
||||||
userId: number;
|
userId: number;
|
||||||
organisationId: string;
|
organisationId: string;
|
||||||
organisationGroups: OrganisationGroup[];
|
organisationGroups: OrganisationGroup[];
|
||||||
organisationMemberRole: OrganisationMemberRole;
|
organisationMemberRole: OrganisationMemberRole;
|
||||||
|
bypassEmail?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const organisationGroupToUse = organisationGroups.find(
|
const organisationGroupToUse = organisationGroups.find(
|
||||||
(group) =>
|
(group) =>
|
||||||
@ -122,6 +124,7 @@ export const addUserToOrganisation = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!bypassEmail) {
|
||||||
await jobs.triggerJob({
|
await jobs.triggerJob({
|
||||||
name: 'send.organisation-member-joined.email',
|
name: 'send.organisation-member-joined.email',
|
||||||
payload: {
|
payload: {
|
||||||
@ -129,6 +132,7 @@ export const addUserToOrganisation = async ({
|
|||||||
memberUserId: userId,
|
memberUserId: userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ timeout: 30_000 },
|
{ timeout: 30_000 },
|
||||||
);
|
);
|
||||||
|
|||||||
@ -32,10 +32,8 @@ export const insertFieldInPDFV2 = async ({
|
|||||||
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
|
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
|
||||||
const layer = new Konva.Layer();
|
const layer = new Konva.Layer();
|
||||||
|
|
||||||
const insertedFields = fields.filter((field) => field.inserted);
|
|
||||||
|
|
||||||
// Render the fields onto the layer.
|
// Render the fields onto the layer.
|
||||||
for (const field of insertedFields) {
|
for (const field of fields) {
|
||||||
renderField({
|
renderField({
|
||||||
scale: 1,
|
scale: 1,
|
||||||
field: {
|
field: {
|
||||||
|
|||||||
@ -215,6 +215,12 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
|
const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
|
||||||
const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);
|
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
|
// Include if it's required or has a signed value
|
||||||
return isRequiredField(templateField) || signedFieldValue !== undefined;
|
return isRequiredField(templateField) || signedFieldValue !== undefined;
|
||||||
});
|
});
|
||||||
@ -468,7 +474,15 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
signingOrder: directTemplateRecipient.signingOrder,
|
signingOrder: directTemplateRecipient.signingOrder,
|
||||||
fields: {
|
fields: {
|
||||||
createMany: {
|
createMany: {
|
||||||
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
|
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,
|
envelopeId: createdEnvelope.id,
|
||||||
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
|
||||||
type: templateField.type,
|
type: templateField.type,
|
||||||
@ -478,9 +492,10 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
width: templateField.width,
|
width: templateField.width,
|
||||||
height: templateField.height,
|
height: templateField.height,
|
||||||
customText: customText ?? '',
|
customText: customText ?? '',
|
||||||
inserted: true,
|
inserted,
|
||||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,9 +1,46 @@
|
|||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
import { FieldType } from '@prisma/client';
|
import { FieldType } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '../constants/pdf';
|
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({
|
export const ZBaseFieldMeta = z.object({
|
||||||
label: z.string().optional(),
|
label: z.string().optional(),
|
||||||
@ -50,8 +87,14 @@ export type TDateFieldMeta = z.infer<typeof ZDateFieldMeta>;
|
|||||||
export const ZTextFieldMeta = ZBaseFieldMeta.extend({
|
export const ZTextFieldMeta = ZBaseFieldMeta.extend({
|
||||||
type: z.literal('text'),
|
type: z.literal('text'),
|
||||||
text: z.string().optional(),
|
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(),
|
textAlign: ZFieldTextAlignSchema.optional(),
|
||||||
|
lineHeight: ZFieldMetaLineHeight.nullish(),
|
||||||
|
letterSpacing: ZFieldMetaLetterSpacing.nullish(),
|
||||||
|
verticalAlign: ZFieldMetaVerticalAlign.nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
|
export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
|
||||||
@ -63,6 +106,9 @@ export const ZNumberFieldMeta = ZBaseFieldMeta.extend({
|
|||||||
minValue: z.coerce.number().nullish(),
|
minValue: z.coerce.number().nullish(),
|
||||||
maxValue: z.coerce.number().nullish(),
|
maxValue: z.coerce.number().nullish(),
|
||||||
textAlign: ZFieldTextAlignSchema.optional(),
|
textAlign: ZFieldTextAlignSchema.optional(),
|
||||||
|
lineHeight: ZFieldMetaLineHeight.nullish(),
|
||||||
|
letterSpacing: ZFieldMetaLetterSpacing.nullish(),
|
||||||
|
verticalAlign: ZFieldMetaVerticalAlign.nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TNumberFieldMeta = z.infer<typeof ZNumberFieldMeta>;
|
export type TNumberFieldMeta = z.infer<typeof ZNumberFieldMeta>;
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export const renderCheckboxFieldElement = (
|
|||||||
field: FieldToRender,
|
field: FieldToRender,
|
||||||
options: RenderFieldElementOptions,
|
options: RenderFieldElementOptions,
|
||||||
) => {
|
) => {
|
||||||
const { pageWidth, pageHeight, pageLayer, mode } = options;
|
const { pageWidth, pageHeight, pageLayer, mode, color } = options;
|
||||||
|
|
||||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||||
|
|
||||||
@ -210,7 +210,9 @@ export const renderCheckboxFieldElement = (
|
|||||||
fieldGroup.add(text);
|
fieldGroup.add(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (color !== 'readOnly' && mode !== 'export') {
|
||||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fieldGroup,
|
fieldGroup,
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export const renderDropdownFieldElement = (
|
|||||||
field: FieldToRender,
|
field: FieldToRender,
|
||||||
options: RenderFieldElementOptions,
|
options: RenderFieldElementOptions,
|
||||||
) => {
|
) => {
|
||||||
const { pageWidth, pageHeight, pageLayer, mode, translations } = options;
|
const { pageWidth, pageHeight, pageLayer, mode, translations, color } = options;
|
||||||
|
|
||||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
||||||
|
|
||||||
@ -74,6 +74,21 @@ export const renderDropdownFieldElement = (
|
|||||||
|
|
||||||
const fontSize = dropdownMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
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) {
|
if (field.inserted) {
|
||||||
selectedValue = field.customText;
|
selectedValue = field.customText;
|
||||||
}
|
}
|
||||||
@ -166,7 +181,9 @@ export const renderDropdownFieldElement = (
|
|||||||
pageLayer.batchDraw();
|
pageLayer.batchDraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (color !== 'readOnly' && mode !== 'export') {
|
||||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fieldGroup,
|
fieldGroup,
|
||||||
|
|||||||
@ -77,6 +77,7 @@ export const renderField = ({
|
|||||||
scale,
|
scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If the generic text field element array changes, update the `GenericTextFieldTypeMetas` type
|
||||||
return match(field.type)
|
return match(field.type)
|
||||||
.with(
|
.with(
|
||||||
FieldType.INITIALS,
|
FieldType.INITIALS,
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
|
||||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
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 {
|
import {
|
||||||
createFieldHoverInteraction,
|
createFieldHoverInteraction,
|
||||||
konvaTextFill,
|
konvaTextFill,
|
||||||
@ -12,14 +18,14 @@ import {
|
|||||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||||
import { calculateFieldPosition } 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 upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
||||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
||||||
|
|
||||||
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
|
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;
|
const fieldTypeName = translations?.[field.type] || field.type;
|
||||||
|
|
||||||
@ -33,53 +39,77 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
|||||||
// Calculate text positioning based on alignment
|
// Calculate text positioning based on alignment
|
||||||
const textX = 0;
|
const textX = 0;
|
||||||
const textY = 0;
|
const textY = 0;
|
||||||
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN;
|
const textFontSize = fieldMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||||
const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle';
|
|
||||||
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
|
||||||
const textPadding = 10;
|
|
||||||
|
|
||||||
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.
|
// Render default values for text/number if provided for editing mode.
|
||||||
if (mode === 'edit') {
|
if (mode === 'edit' && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) {
|
||||||
if (textMeta?.text) {
|
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value;
|
||||||
textToRender = textMeta.text;
|
|
||||||
} else {
|
if (value) {
|
||||||
// Show field name which is centered for the edit mode if no label/text is avaliable.
|
textToRender = value;
|
||||||
textToRender = textMeta?.label || fieldTypeName;
|
|
||||||
textAlign = 'center';
|
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.
|
// Default to blank for export mode since we want to ensure we don't show
|
||||||
if (mode === 'sign' || mode === 'export') {
|
// any placeholder text or labels unless actually it's inserted.
|
||||||
if (!field.inserted) {
|
if (mode === 'export') {
|
||||||
if (textMeta?.text) {
|
textToRender = '';
|
||||||
textToRender = textMeta.text;
|
}
|
||||||
} else if (mode === 'sign') {
|
|
||||||
// Only show the field name in sign mode if no text/label is avaliable.
|
// Fallback render readonly fields if prefilled value exists.
|
||||||
textToRender = textMeta?.label || fieldTypeName;
|
if (field?.fieldMeta?.readOnly && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) {
|
||||||
textAlign = 'center';
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override everything with value if it's inserted.
|
||||||
if (field.inserted) {
|
if (field.inserted) {
|
||||||
textToRender = field.customText;
|
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({
|
fieldText.setAttrs({
|
||||||
x: textX,
|
x: textX + DEFAULT_TEXT_X_PADDING,
|
||||||
y: textY,
|
y: textY,
|
||||||
verticalAlign: textVerticalAlign,
|
verticalAlign: textVerticalAlign,
|
||||||
wrap: 'word',
|
wrap: 'word',
|
||||||
padding: textPadding,
|
|
||||||
text: textToRender,
|
text: textToRender,
|
||||||
fontSize: textFontSize,
|
fontSize: textFontSize,
|
||||||
|
align: textAlign,
|
||||||
|
lineHeight: textLineHeight,
|
||||||
|
letterSpacing: textLetterSpacing,
|
||||||
fontFamily: konvaTextFontFamily,
|
fontFamily: konvaTextFontFamily,
|
||||||
fill: konvaTextFill,
|
fill: konvaTextFill,
|
||||||
align: textAlign,
|
width: fieldWidth - DEFAULT_TEXT_X_PADDING * 2,
|
||||||
width: fieldWidth,
|
|
||||||
height: fieldHeight,
|
height: fieldHeight,
|
||||||
} satisfies Partial<Konva.TextConfig>);
|
} satisfies Partial<Konva.TextConfig>);
|
||||||
|
|
||||||
@ -90,7 +120,7 @@ export const renderGenericTextFieldElement = (
|
|||||||
field: FieldToRender,
|
field: FieldToRender,
|
||||||
options: RenderFieldElementOptions,
|
options: RenderFieldElementOptions,
|
||||||
) => {
|
) => {
|
||||||
const { mode = 'edit', pageLayer } = options;
|
const { mode = 'edit', pageLayer, color } = options;
|
||||||
|
|
||||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||||
|
|
||||||
@ -125,7 +155,7 @@ export const renderGenericTextFieldElement = (
|
|||||||
const rectHeight = fieldRect.height() * groupScaleY;
|
const rectHeight = fieldRect.height() * groupScaleY;
|
||||||
|
|
||||||
// Update text dimensions
|
// Update text dimensions
|
||||||
fieldText.width(rectWidth);
|
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2);
|
||||||
fieldText.height(rectHeight);
|
fieldText.height(rectHeight);
|
||||||
|
|
||||||
// Force Konva to recalculate text layout
|
// Force Konva to recalculate text layout
|
||||||
@ -143,7 +173,7 @@ export const renderGenericTextFieldElement = (
|
|||||||
const rectHeight = fieldRect.height();
|
const rectHeight = fieldRect.height();
|
||||||
|
|
||||||
// Update text dimensions
|
// Update text dimensions
|
||||||
fieldText.width(rectWidth); // Account for padding
|
fieldText.width(rectWidth - DEFAULT_TEXT_X_PADDING * 2);
|
||||||
fieldText.height(rectHeight);
|
fieldText.height(rectHeight);
|
||||||
|
|
||||||
// Force Konva to recalculate text layout
|
// Force Konva to recalculate text layout
|
||||||
@ -158,7 +188,9 @@ export const renderGenericTextFieldElement = (
|
|||||||
fieldRect.opacity(0);
|
fieldRect.opacity(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (color !== 'readOnly' && mode !== 'export') {
|
||||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fieldGroup,
|
fieldGroup,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export const renderRadioFieldElement = (
|
|||||||
field: FieldToRender,
|
field: FieldToRender,
|
||||||
options: RenderFieldElementOptions,
|
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 radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
|
||||||
const radioValues = radioMeta?.values || [];
|
const radioValues = radioMeta?.values || [];
|
||||||
@ -195,7 +195,9 @@ export const renderRadioFieldElement = (
|
|||||||
fieldGroup.add(text);
|
fieldGroup.add(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (color !== 'readOnly' && mode !== 'export') {
|
||||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fieldGroup,
|
fieldGroup,
|
||||||
|
|||||||
@ -142,7 +142,7 @@ export const renderSignatureFieldElement = (
|
|||||||
field: FieldToRender,
|
field: FieldToRender,
|
||||||
options: RenderFieldElementOptions,
|
options: RenderFieldElementOptions,
|
||||||
) => {
|
) => {
|
||||||
const { mode = 'edit', pageLayer } = options;
|
const { mode = 'edit', pageLayer, color } = options;
|
||||||
|
|
||||||
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
|
||||||
|
|
||||||
@ -211,7 +211,9 @@ export const renderSignatureFieldElement = (
|
|||||||
fieldRect.opacity(0);
|
fieldRect.opacity(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (color !== 'readOnly' && mode !== 'export') {
|
||||||
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fieldGroup,
|
fieldGroup,
|
||||||
|
|||||||
@ -2,18 +2,33 @@ import type { EnvelopeItem } from '@prisma/client';
|
|||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||||
|
|
||||||
export type EnvelopeDownloadUrlOptions = {
|
export type EnvelopeItemPdfUrlOptions =
|
||||||
|
| {
|
||||||
|
type: 'download';
|
||||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||||
token: string | undefined;
|
token: string | undefined;
|
||||||
version: 'original' | 'signed';
|
version: 'original' | 'signed';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'view';
|
||||||
|
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||||
|
token: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEnvelopeDownloadUrl = (options: EnvelopeDownloadUrlOptions) => {
|
export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
|
||||||
const { envelopeItem, token, version } = options;
|
const { envelopeItem, token, type } = options;
|
||||||
|
|
||||||
const { id, envelopeId } = envelopeItem;
|
const { id, envelopeId } = envelopeItem;
|
||||||
|
|
||||||
|
if (type === 'download') {
|
||||||
|
const version = options.version;
|
||||||
|
|
||||||
return token
|
return token
|
||||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
|
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
|
||||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
|
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}`
|
||||||
|
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -102,7 +102,7 @@ export const extractFieldInsertionValues = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
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) {
|
if (errors.length > 0) {
|
||||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
@ -111,7 +111,7 @@ export const extractFieldInsertionValues = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customText: fieldValue.value.toString(),
|
customText: fieldValue.value,
|
||||||
inserted: true,
|
inserted: true,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import type { Envelope, Recipient } from '@prisma/client';
|
import type { Envelope, Recipient } from '@prisma/client';
|
||||||
import { DocumentStatus, EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
|
import {
|
||||||
|
DocumentStatus,
|
||||||
|
EnvelopeType,
|
||||||
|
RecipientRole,
|
||||||
|
SendStatus,
|
||||||
|
SigningStatus,
|
||||||
|
} from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@ -156,8 +162,9 @@ export const canEnvelopeItemsBeModified = (
|
|||||||
if (
|
if (
|
||||||
recipients.some(
|
recipients.some(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.signingStatus === SigningStatus.SIGNED ||
|
recipient.role !== RecipientRole.CC &&
|
||||||
recipient.sendStatus === SendStatus.SENT,
|
(recipient.signingStatus === SigningStatus.SIGNED ||
|
||||||
|
recipient.sendStatus === SendStatus.SENT),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
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 { FIELD_META_TEST_FIELDS } from '@documenso/app-tests/constants/field-meta-pdf';
|
||||||
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
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 { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||||
|
|
||||||
import { prisma } from '..';
|
import { prisma } from '..';
|
||||||
|
import {
|
||||||
|
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||||
|
DIRECT_TEMPLATE_RECIPIENT_NAME,
|
||||||
|
} from '../../lib/constants/direct-templates';
|
||||||
import {
|
import {
|
||||||
DocumentDataType,
|
DocumentDataType,
|
||||||
DocumentSource,
|
DocumentSource,
|
||||||
@ -176,6 +183,26 @@ export const seedDatabase = async () => {
|
|||||||
userId: adminUser.user.id,
|
userId: adminUser.user.id,
|
||||||
teamId: adminUser.team.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({
|
seedAlignmentTestDocument({
|
||||||
userId: exampleUser.user.id,
|
userId: exampleUser.user.id,
|
||||||
teamId: exampleUser.team.id,
|
teamId: exampleUser.team.id,
|
||||||
@ -192,6 +219,26 @@ export const seedDatabase = async () => {
|
|||||||
insertFields: true,
|
insertFields: true,
|
||||||
status: DocumentStatus.PENDING,
|
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({
|
seedAlignmentTestDocument({
|
||||||
userId: adminUser.user.id,
|
userId: adminUser.user.id,
|
||||||
teamId: adminUser.team.id,
|
teamId: adminUser.team.id,
|
||||||
@ -214,17 +261,25 @@ export const seedDatabase = async () => {
|
|||||||
export const seedAlignmentTestDocument = async ({
|
export const seedAlignmentTestDocument = async ({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
|
title = 'Envelope Full Field Test',
|
||||||
recipientName,
|
recipientName,
|
||||||
recipientEmail,
|
recipientEmail,
|
||||||
insertFields,
|
insertFields,
|
||||||
status,
|
status,
|
||||||
|
type = EnvelopeType.DOCUMENT,
|
||||||
|
isDirectTemplate = false,
|
||||||
|
directTemplateToken,
|
||||||
}: {
|
}: {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
|
title?: string;
|
||||||
recipientName: string;
|
recipientName: string;
|
||||||
recipientEmail: string;
|
recipientEmail: string;
|
||||||
insertFields: boolean;
|
insertFields: boolean;
|
||||||
status: DocumentStatus;
|
status: DocumentStatus;
|
||||||
|
type?: EnvelopeType;
|
||||||
|
isDirectTemplate?: boolean;
|
||||||
|
directTemplateToken?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const alignmentPdf = fs
|
const alignmentPdf = fs
|
||||||
.readFileSync(path.join(__dirname, '../../../assets/field-font-alignment.pdf'))
|
.readFileSync(path.join(__dirname, '../../../assets/field-font-alignment.pdf'))
|
||||||
@ -237,7 +292,10 @@ export const seedAlignmentTestDocument = async ({
|
|||||||
const alignmentDocumentData = await createDocumentData({ documentData: alignmentPdf });
|
const alignmentDocumentData = await createDocumentData({ documentData: alignmentPdf });
|
||||||
const fieldMetaDocumentData = await createDocumentData({ documentData: fieldMetaPdf });
|
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({
|
const documentMeta = await prisma.documentMeta.create({
|
||||||
data: {},
|
data: {},
|
||||||
@ -246,12 +304,12 @@ export const seedAlignmentTestDocument = async ({
|
|||||||
const createdEnvelope = await prisma.envelope.create({
|
const createdEnvelope = await prisma.envelope.create({
|
||||||
data: {
|
data: {
|
||||||
id: prefixedId('envelope'),
|
id: prefixedId('envelope'),
|
||||||
secondaryId: documentId.formattedDocumentId,
|
secondaryId,
|
||||||
internalVersion: 2,
|
internalVersion: 2,
|
||||||
type: EnvelopeType.DOCUMENT,
|
type,
|
||||||
documentMetaId: documentMeta.id,
|
documentMetaId: documentMeta.id,
|
||||||
source: DocumentSource.DOCUMENT,
|
source: DocumentSource.DOCUMENT,
|
||||||
title: `Envelope Full Field Test`,
|
title,
|
||||||
status,
|
status,
|
||||||
envelopeItems: {
|
envelopeItems: {
|
||||||
createMany: {
|
createMany: {
|
||||||
@ -275,8 +333,8 @@ export const seedAlignmentTestDocument = async ({
|
|||||||
teamId,
|
teamId,
|
||||||
recipients: {
|
recipients: {
|
||||||
create: {
|
create: {
|
||||||
name: recipientName,
|
name: isDirectTemplate ? DIRECT_TEMPLATE_RECIPIENT_NAME : recipientName,
|
||||||
email: recipientEmail,
|
email: isDirectTemplate ? DIRECT_TEMPLATE_RECIPIENT_EMAIL : recipientEmail,
|
||||||
token: nanoid(),
|
token: nanoid(),
|
||||||
sendStatus: status === 'DRAFT' ? SendStatus.NOT_SENT : SendStatus.SENT,
|
sendStatus: status === 'DRAFT' ? SendStatus.NOT_SENT : SendStatus.SENT,
|
||||||
signingStatus: status === 'COMPLETED' ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
signingStatus: status === 'COMPLETED' ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||||
@ -292,6 +350,25 @@ export const seedAlignmentTestDocument = async ({
|
|||||||
|
|
||||||
const { id, recipients, envelopeItems } = createdEnvelope;
|
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 recipientId = recipients[0].id;
|
||||||
const envelopeItemAlignmentItem = envelopeItems.find((item) => item.order === 1)?.id;
|
const envelopeItemAlignmentItem = envelopeItems.find((item) => item.order === 1)?.id;
|
||||||
const envelopeItemFieldMetaItem = envelopeItems.find((item) => item.order === 2)?.id;
|
const envelopeItemFieldMetaItem = envelopeItems.find((item) => item.order === 2)?.id;
|
||||||
@ -301,7 +378,7 @@ export const seedAlignmentTestDocument = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
formatAlignmentTestFields.map(async (field) => {
|
ALIGNMENT_TEST_FIELDS.map(async (field) => {
|
||||||
await prisma.field.create({
|
await prisma.field.create({
|
||||||
data: {
|
data: {
|
||||||
...field,
|
...field,
|
||||||
@ -309,8 +386,12 @@ export const seedAlignmentTestDocument = async ({
|
|||||||
envelopeItemId: envelopeItemAlignmentItem,
|
envelopeItemId: envelopeItemAlignmentItem,
|
||||||
envelopeId: id,
|
envelopeId: id,
|
||||||
customText: insertFields ? field.customText : '',
|
customText: insertFields ? field.customText : '',
|
||||||
inserted: insertFields,
|
inserted:
|
||||||
signature: field.signature
|
insertFields &&
|
||||||
|
((!field?.fieldMeta?.readOnly && Boolean(field.customText)) ||
|
||||||
|
field.type === 'SIGNATURE'),
|
||||||
|
signature:
|
||||||
|
field.signature && insertFields
|
||||||
? {
|
? {
|
||||||
create: {
|
create: {
|
||||||
recipientId,
|
recipientId,
|
||||||
@ -333,8 +414,12 @@ export const seedAlignmentTestDocument = async ({
|
|||||||
envelopeItemId: envelopeItemFieldMetaItem,
|
envelopeItemId: envelopeItemFieldMetaItem,
|
||||||
envelopeId: id,
|
envelopeId: id,
|
||||||
customText: insertFields ? field.customText : '',
|
customText: insertFields ? field.customText : '',
|
||||||
inserted: insertFields,
|
inserted:
|
||||||
signature: field.signature
|
insertFields &&
|
||||||
|
((!field?.fieldMeta?.readOnly && Boolean(field.customText)) ||
|
||||||
|
field.type === 'SIGNATURE'),
|
||||||
|
signature:
|
||||||
|
field.signature && insertFields
|
||||||
? {
|
? {
|
||||||
create: {
|
create: {
|
||||||
recipientId,
|
recipientId,
|
||||||
|
|||||||
@ -64,6 +64,7 @@ export const seedOrganisationMembers = async ({
|
|||||||
organisationId,
|
organisationId,
|
||||||
organisationGroups,
|
organisationGroups,
|
||||||
organisationMemberRole: member.organisationRole,
|
organisationMemberRole: member.organisationRole,
|
||||||
|
bypassEmail: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export const ZSignEnvelopeFieldValue = z.discriminatedUnion('type', [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal(FieldType.NUMBER),
|
type: z.literal(FieldType.NUMBER),
|
||||||
value: z.number().nullable(),
|
value: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal(FieldType.EMAIL),
|
type: z.literal(FieldType.EMAIL),
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { SigningStatus } from '@prisma/client';
|
import { SigningStatus } from '@prisma/client';
|
||||||
import type { Field, Recipient } 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 { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
@ -20,7 +20,15 @@ import { PopoverHover } from '../../primitives/popover';
|
|||||||
interface EnvelopeRecipientFieldTooltipProps {
|
interface EnvelopeRecipientFieldTooltipProps {
|
||||||
field: Pick<
|
field: Pick<
|
||||||
Field,
|
Field,
|
||||||
'id' | 'inserted' | 'positionX' | 'positionY' | 'width' | 'height' | 'page' | 'type'
|
| 'id'
|
||||||
|
| 'inserted'
|
||||||
|
| 'positionX'
|
||||||
|
| 'positionY'
|
||||||
|
| 'width'
|
||||||
|
| 'height'
|
||||||
|
| 'page'
|
||||||
|
| 'type'
|
||||||
|
| 'fieldMeta'
|
||||||
> & {
|
> & {
|
||||||
recipient: Pick<Recipient, 'name' | 'email' | 'signingStatus'>;
|
recipient: Pick<Recipient, 'name' | 'email' | 'signingStatus'>;
|
||||||
};
|
};
|
||||||
@ -151,10 +159,19 @@ export function EnvelopeRecipientFieldTooltip({
|
|||||||
<Badge
|
<Badge
|
||||||
className="mx-auto mb-1 py-0.5"
|
className="mx-auto mb-1 py-0.5"
|
||||||
variant={
|
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" />
|
<SignatureIcon className="mr-1 h-3 w-3" />
|
||||||
<Trans>Signed</Trans>
|
<Trans>Signed</Trans>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background border-input ring-offset-background placeholder:text-muted-foreground/40 focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
'bg-background border-input ring-offset-background placeholder:text-muted-foreground/40 focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||||
className,
|
className,
|
||||||
{
|
{
|
||||||
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],
|
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
|||||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
|
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||||
|
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import { useToast } from './use-toast';
|
import { useToast } from './use-toast';
|
||||||
@ -157,10 +157,10 @@ export const PDFViewer = ({
|
|||||||
try {
|
try {
|
||||||
setIsDocumentBytesLoading(true);
|
setIsDocumentBytesLoading(true);
|
||||||
|
|
||||||
const documentUrl = getEnvelopeDownloadUrl({
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'view',
|
||||||
envelopeItem: envelopeItem,
|
envelopeItem: envelopeItem,
|
||||||
token,
|
token,
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-popover text-popover-foreground animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
|
'bg-popover z-9999 text-popover-foreground animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||