mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
chore: add more field types (#1141)
Adds a number of new field types and capabilities to existing fields. A massive change with far too many moving pieces to document in a single commit.
This commit is contained in:
82
packages/lib/advanced-fields-validation/validate-checkbox.ts
Normal file
82
packages/lib/advanced-fields-validation/validate-checkbox.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
||||
|
||||
interface CheckboxFieldMeta {
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
validationRule?: string;
|
||||
validationLength?: number;
|
||||
}
|
||||
|
||||
export const validateCheckboxField = (
|
||||
values: string[],
|
||||
fieldMeta: CheckboxFieldMeta,
|
||||
isSigningPage: boolean = false,
|
||||
): string[] => {
|
||||
const errors = [];
|
||||
|
||||
const { readOnly, required, validationRule, validationLength } = fieldMeta;
|
||||
|
||||
if (readOnly && required) {
|
||||
errors.push('A field cannot be both read-only and required');
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
errors.push('At least one option must be added');
|
||||
}
|
||||
|
||||
if (readOnly && values.length === 0) {
|
||||
errors.push('A read-only field must have at least one value');
|
||||
}
|
||||
|
||||
if (isSigningPage && required && values.length === 0) {
|
||||
errors.push('Selecting an option is required');
|
||||
}
|
||||
|
||||
if (validationRule && !validationLength) {
|
||||
errors.push('You need to specify the number of options for validation');
|
||||
}
|
||||
|
||||
if (validationLength && !validationRule) {
|
||||
errors.push('You need to specify the validation rule');
|
||||
}
|
||||
|
||||
if (validationRule && validationLength) {
|
||||
const validation = checkboxValidationSigns.find((sign) => sign.label === validationRule);
|
||||
|
||||
if (validation) {
|
||||
let lengthCondition = false;
|
||||
|
||||
switch (validation.value) {
|
||||
case '=':
|
||||
lengthCondition = isSigningPage
|
||||
? values.length !== validationLength
|
||||
: values.length < validationLength;
|
||||
break;
|
||||
case '>=':
|
||||
lengthCondition = values.length < validationLength;
|
||||
break;
|
||||
case '<=':
|
||||
lengthCondition = isSigningPage
|
||||
? values.length > validationLength
|
||||
: values.length < validationLength;
|
||||
break;
|
||||
}
|
||||
|
||||
if (lengthCondition) {
|
||||
let errorMessage;
|
||||
if (isSigningPage) {
|
||||
errorMessage = `You need to ${validationRule.toLowerCase()} ${validationLength} options`;
|
||||
} else {
|
||||
errorMessage =
|
||||
validation.value === '<='
|
||||
? `You need to select at least ${validationLength} options`
|
||||
: `You need to add at least ${validationLength} options`;
|
||||
}
|
||||
|
||||
errors.push(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
54
packages/lib/advanced-fields-validation/validate-dropdown.ts
Normal file
54
packages/lib/advanced-fields-validation/validate-dropdown.ts
Normal file
@ -0,0 +1,54 @@
|
||||
interface DropdownFieldMeta {
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
values?: { value: string }[];
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export const validateDropdownField = (
|
||||
value: string | undefined,
|
||||
fieldMeta: DropdownFieldMeta,
|
||||
isSigningPage: boolean = false,
|
||||
): string[] => {
|
||||
const errors = [];
|
||||
|
||||
const { readOnly, required, values, defaultValue } = fieldMeta;
|
||||
|
||||
if (readOnly && required) {
|
||||
errors.push('A field cannot be both read-only and required');
|
||||
}
|
||||
|
||||
if (readOnly && (!values || values.length === 0)) {
|
||||
errors.push('A read-only field must have at least one value');
|
||||
}
|
||||
|
||||
if (isSigningPage && required && !value) {
|
||||
errors.push('Choosing an option is required');
|
||||
}
|
||||
|
||||
if (values && values.length === 0) {
|
||||
errors.push('Select field must have at least one option');
|
||||
}
|
||||
|
||||
if (values && values.length === 0 && defaultValue) {
|
||||
errors.push('Default value must be one of the available options');
|
||||
}
|
||||
|
||||
if (value && values && !values.find((item) => item.value === value)) {
|
||||
errors.push('Selected value must be one of the available options');
|
||||
}
|
||||
|
||||
if (values && defaultValue && !values.find((item) => item.value === defaultValue)) {
|
||||
errors.push('Default value must be one of the available options');
|
||||
}
|
||||
|
||||
if (values && values.some((item) => item.value.length < 1)) {
|
||||
errors.push('Option value cannot be empty');
|
||||
}
|
||||
|
||||
if (values && new Set(values.map((item) => item.value)).size !== values.length) {
|
||||
errors.push('Duplicate values are not allowed');
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
67
packages/lib/advanced-fields-validation/validate-number.ts
Normal file
67
packages/lib/advanced-fields-validation/validate-number.ts
Normal file
@ -0,0 +1,67 @@
|
||||
// import { numberFormatValues } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
||||
|
||||
interface NumberFieldMeta {
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
numberFormat?: string;
|
||||
}
|
||||
|
||||
export const validateNumberField = (
|
||||
value: string,
|
||||
fieldMeta?: NumberFieldMeta,
|
||||
isSigningPage: boolean = false,
|
||||
): string[] => {
|
||||
const errors = [];
|
||||
|
||||
const { minValue, maxValue, readOnly, required, numberFormat } = fieldMeta || {};
|
||||
|
||||
const formatRegex: { [key: string]: RegExp } = {
|
||||
'123,456,789.00': /^(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d{1,2})?$/,
|
||||
'123.456.789,00': /^(?:\d{1,3}(?:\.\d{3})*|\d+)(?:,\d{1,2})?$/,
|
||||
'123456,789.00': /^(?:\d+)(?:,\d{1,3}(?:\.\d{1,2})?)?$/,
|
||||
};
|
||||
|
||||
const isValidFormat = numberFormat ? formatRegex[numberFormat].test(value) : true;
|
||||
|
||||
if (!isValidFormat) {
|
||||
errors.push(`Value ${value} does not match the number format - ${numberFormat}`);
|
||||
}
|
||||
|
||||
const numberValue = parseFloat(value);
|
||||
|
||||
if (isSigningPage && required && !value) {
|
||||
errors.push('Value is required');
|
||||
}
|
||||
|
||||
if (!/^[0-9,.]+$/.test(value.trim())) {
|
||||
errors.push(`Value is not a valid number`);
|
||||
}
|
||||
|
||||
if (minValue !== undefined && minValue > 0 && numberValue < minValue) {
|
||||
errors.push(`Value ${value} is less than the minimum value of ${minValue}`);
|
||||
}
|
||||
|
||||
if (maxValue !== undefined && maxValue > 0 && numberValue > maxValue) {
|
||||
errors.push(`Value ${value} is greater than the maximum value of ${maxValue}`);
|
||||
}
|
||||
|
||||
if (minValue !== undefined && maxValue !== undefined && minValue > maxValue) {
|
||||
errors.push('Minimum value cannot be greater than maximum value');
|
||||
}
|
||||
|
||||
if (maxValue !== undefined && minValue !== undefined && maxValue < minValue) {
|
||||
errors.push('Maximum value cannot be less than minimum value');
|
||||
}
|
||||
|
||||
if (readOnly && numberValue < 1) {
|
||||
errors.push('A read-only field must have a value greater than 0');
|
||||
}
|
||||
|
||||
if (readOnly && required) {
|
||||
errors.push('A field cannot be both read-only and required');
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
41
packages/lib/advanced-fields-validation/validate-radio.ts
Normal file
41
packages/lib/advanced-fields-validation/validate-radio.ts
Normal file
@ -0,0 +1,41 @@
|
||||
interface RadioFieldMeta {
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
values?: { checked: boolean; value: string }[];
|
||||
}
|
||||
|
||||
export const validateRadioField = (
|
||||
value: string | undefined,
|
||||
fieldMeta: RadioFieldMeta,
|
||||
isSigningPage: boolean = false,
|
||||
): string[] => {
|
||||
const errors = [];
|
||||
|
||||
const { readOnly, required, values } = fieldMeta;
|
||||
|
||||
if (readOnly && required) {
|
||||
errors.push('A field cannot be both read-only and required');
|
||||
}
|
||||
|
||||
if (readOnly && (!values || values.length === 0)) {
|
||||
errors.push('A read-only field must have at least one value');
|
||||
}
|
||||
|
||||
if (isSigningPage && required && !value) {
|
||||
errors.push('Choosing an option is required');
|
||||
}
|
||||
|
||||
if (values) {
|
||||
const checkedRadioFieldValues = values.filter((option) => option.checked);
|
||||
|
||||
if (values.length === 0) {
|
||||
errors.push('Radio field must have at least one option');
|
||||
}
|
||||
|
||||
if (checkedRadioFieldValues.length > 1) {
|
||||
errors.push('There cannot be more than one checked option');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
33
packages/lib/advanced-fields-validation/validate-text.ts
Normal file
33
packages/lib/advanced-fields-validation/validate-text.ts
Normal file
@ -0,0 +1,33 @@
|
||||
interface TextFieldMeta {
|
||||
characterLimit?: number;
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const validateTextField = (
|
||||
value: string,
|
||||
fieldMeta: TextFieldMeta,
|
||||
isSigningPage: boolean = false,
|
||||
): string[] => {
|
||||
const errors = [];
|
||||
|
||||
const { characterLimit, readOnly, required } = fieldMeta;
|
||||
|
||||
if (required && !value && isSigningPage) {
|
||||
errors.push('Value is required');
|
||||
}
|
||||
|
||||
if (characterLimit !== undefined && characterLimit > 0 && value.length > characterLimit) {
|
||||
errors.push(`Value length (${value.length}) exceeds the character limit (${characterLimit})`);
|
||||
}
|
||||
|
||||
if (readOnly && value.length < 1) {
|
||||
errors.push('A read-only field must have text');
|
||||
}
|
||||
|
||||
if (readOnly && required) {
|
||||
errors.push('A field cannot be both read-only and required');
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
25
packages/lib/client-only/hooks/use-field-item-styles.ts
Normal file
25
packages/lib/client-only/hooks/use-field-item-styles.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { CombinedStylesKey } from '../../../ui/primitives/document-flow/add-fields';
|
||||
import { combinedStyles } from '../../../ui/primitives/document-flow/field-item';
|
||||
|
||||
const defaultFieldItemStyles = {
|
||||
borderClass: 'border-field-card-border',
|
||||
activeBorderClass: 'border-field-card-border/80',
|
||||
initialsBGClass: 'text-field-card-foreground/50 bg-slate-900/10',
|
||||
fieldBackground: 'bg-field-card-background',
|
||||
};
|
||||
|
||||
export const useFieldItemStyles = (color: CombinedStylesKey | null) => {
|
||||
return useMemo(() => {
|
||||
if (!color) return defaultFieldItemStyles;
|
||||
|
||||
const selectedColorVariant = combinedStyles[color];
|
||||
return {
|
||||
activeBorderClass: selectedColorVariant?.borderActive,
|
||||
borderClass: selectedColorVariant?.border,
|
||||
initialsBGClass: selectedColorVariant?.initialsBG,
|
||||
fieldBackground: selectedColorVariant?.fieldBackground,
|
||||
};
|
||||
}, [color]);
|
||||
};
|
||||
@ -117,6 +117,9 @@ export const sealDocument = async ({
|
||||
await insertFieldInPDF(doc, field);
|
||||
}
|
||||
|
||||
// Re-flatten post-insertion to handle fields that create arcoFields
|
||||
flattenForm(doc);
|
||||
|
||||
const pdfBytes = await doc.save();
|
||||
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||
@ -161,14 +160,7 @@ export const sendDocument = async ({
|
||||
);
|
||||
|
||||
if (allRecipientsHaveNoActionToTake) {
|
||||
const updatedDocument = await updateDocument({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
data: { status: DocumentStatus.COMPLETED },
|
||||
});
|
||||
|
||||
await sealDocument({ documentId: updatedDocument.id, requestMetadata });
|
||||
await sealDocument({ documentId, requestMetadata });
|
||||
|
||||
// Keep the return type the same for the `sendDocument` method
|
||||
return await prisma.document.findFirstOrThrow({
|
||||
|
||||
@ -1,15 +1,47 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetFieldByIdOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
documentId?: number;
|
||||
templateId?: number;
|
||||
};
|
||||
|
||||
export const getFieldById = async ({ fieldId, documentId }: GetFieldByIdOptions) => {
|
||||
export const getFieldById = async ({
|
||||
userId,
|
||||
teamId,
|
||||
fieldId,
|
||||
documentId,
|
||||
templateId,
|
||||
}: GetFieldByIdOptions) => {
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
documentId,
|
||||
templateId,
|
||||
Document: {
|
||||
OR:
|
||||
teamId === undefined
|
||||
? [
|
||||
{
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
teamId,
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,12 +1,26 @@
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import {
|
||||
type TFieldMetaSchema as FieldMeta,
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import {
|
||||
createDocumentAuditLogData,
|
||||
diffFieldChanges,
|
||||
} from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Field, FieldType } from '@documenso/prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import { FieldType, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export interface SetFieldsForDocumentOptions {
|
||||
userId: number;
|
||||
@ -20,6 +34,7 @@ export interface SetFieldsForDocumentOptions {
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
fieldMeta?: FieldMeta;
|
||||
}[];
|
||||
requestMetadata?: RequestMetadata;
|
||||
}
|
||||
@ -103,6 +118,83 @@ export const setFieldsForDocument = async ({
|
||||
linkedFields.map(async (field) => {
|
||||
const fieldSignerEmail = field.signerEmail.toLowerCase();
|
||||
|
||||
const parsedFieldMeta = field.fieldMeta
|
||||
? ZFieldMetaSchema.parse(field.fieldMeta)
|
||||
: undefined;
|
||||
|
||||
if (field.type === FieldType.TEXT && field.fieldMeta) {
|
||||
const textFieldParsedMeta = ZTextFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateTextField(textFieldParsedMeta.text || '', textFieldParsedMeta);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateNumberField(
|
||||
String(numberFieldParsedMeta.value),
|
||||
numberFieldParsedMeta,
|
||||
);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.CHECKBOX) {
|
||||
if (field.fieldMeta) {
|
||||
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateCheckboxField(
|
||||
checkboxFieldParsedMeta?.values?.map((item) => item.value) ?? [],
|
||||
checkboxFieldParsedMeta,
|
||||
);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'To proceed further, please set at least one value for the Checkbox field',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.RADIO) {
|
||||
if (field.fieldMeta) {
|
||||
const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
const checkedRadioFieldValue = radioFieldParsedMeta.values?.find(
|
||||
(option) => option.checked,
|
||||
)?.value;
|
||||
|
||||
const errors = validateRadioField(checkedRadioFieldValue, radioFieldParsedMeta);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('. '));
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'To proceed further, please set at least one value for the Radio field',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DROPDOWN) {
|
||||
if (field.fieldMeta) {
|
||||
const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateDropdownField(undefined, dropdownFieldParsedMeta);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('. '));
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'To proceed further, please set at least one value for the Dropdown field',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const upsertedField = await tx.field.upsert({
|
||||
where: {
|
||||
id: field._persisted?.id ?? -1,
|
||||
@ -114,6 +206,7 @@ export const setFieldsForDocument = async ({
|
||||
positionY: field.pageY,
|
||||
width: field.pageWidth,
|
||||
height: field.pageHeight,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
},
|
||||
create: {
|
||||
type: field.type,
|
||||
@ -124,6 +217,7 @@ export const setFieldsForDocument = async ({
|
||||
height: field.pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
Document: {
|
||||
connect: {
|
||||
id: documentId,
|
||||
|
||||
@ -1,5 +1,19 @@
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
|
||||
import {
|
||||
type TFieldMetaSchema as FieldMeta,
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZFieldMetaSchema,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
export type SetFieldsForTemplateOptions = {
|
||||
userId: number;
|
||||
@ -13,6 +27,7 @@ export type SetFieldsForTemplateOptions = {
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
fieldMeta?: FieldMeta;
|
||||
}[];
|
||||
};
|
||||
|
||||
@ -70,8 +85,60 @@ export const setFieldsForTemplate = async ({
|
||||
const persistedFields = await prisma.$transaction(
|
||||
// Disabling as wrapping promises here causes type issues
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
linkedFields.map((field) =>
|
||||
prisma.field.upsert({
|
||||
linkedFields.map((field) => {
|
||||
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
|
||||
|
||||
if (field.type === FieldType.TEXT && field.fieldMeta) {
|
||||
const textFieldParsedMeta = ZTextFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateTextField(textFieldParsedMeta.text || '', textFieldParsedMeta);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateNumberField(
|
||||
String(numberFieldParsedMeta.value),
|
||||
numberFieldParsedMeta,
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.CHECKBOX && field.fieldMeta) {
|
||||
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateCheckboxField(
|
||||
checkboxFieldParsedMeta?.values?.map((item) => item.value) ?? [],
|
||||
checkboxFieldParsedMeta,
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.RADIO && field.fieldMeta) {
|
||||
const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
const checkedRadioFieldValue = radioFieldParsedMeta.values?.find(
|
||||
(option) => option.checked,
|
||||
)?.value;
|
||||
const errors = validateRadioField(checkedRadioFieldValue, radioFieldParsedMeta);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('. '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DROPDOWN && field.fieldMeta) {
|
||||
const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateDropdownField(undefined, dropdownFieldParsedMeta);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('. '));
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed with upsert operation
|
||||
return prisma.field.upsert({
|
||||
where: {
|
||||
id: field._persisted?.id ?? -1,
|
||||
templateId,
|
||||
@ -82,6 +149,7 @@ export const setFieldsForTemplate = async ({
|
||||
positionY: field.pageY,
|
||||
width: field.pageWidth,
|
||||
height: field.pageHeight,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
},
|
||||
create: {
|
||||
type: field.type,
|
||||
@ -92,6 +160,7 @@ export const setFieldsForTemplate = async ({
|
||||
height: field.pageHeight,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
Template: {
|
||||
connect: {
|
||||
id: templateId,
|
||||
@ -106,8 +175,8 @@ export const setFieldsForTemplate = async ({
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (removedFields.length > 0) {
|
||||
|
||||
@ -3,6 +3,11 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
|
||||
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
@ -10,6 +15,13 @@ import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
@ -87,6 +99,52 @@ export const signFieldWithToken = async ({
|
||||
throw new Error(`Field ${fieldId} has no recipientId`);
|
||||
}
|
||||
|
||||
if (field.type === FieldType.NUMBER && field.fieldMeta) {
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateNumberField(value, numberFieldParsedMeta, true);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.TEXT && field.fieldMeta) {
|
||||
const textFieldParsedMeta = ZTextFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateTextField(value, textFieldParsedMeta, true);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.CHECKBOX && field.fieldMeta) {
|
||||
const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||
const checkboxFieldValues = value.split(',');
|
||||
const errors = validateCheckboxField(checkboxFieldValues, checkboxFieldParsedMeta, true);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.RADIO && field.fieldMeta) {
|
||||
const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateRadioField(value, radioFieldParsedMeta, true);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === FieldType.DROPDOWN && field.fieldMeta) {
|
||||
const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateDropdownField(value, dropdownFieldParsedMeta, true);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: document.authOptions,
|
||||
recipient,
|
||||
@ -177,6 +235,16 @@ export const signFieldWithToken = async ({
|
||||
type,
|
||||
data: updatedField.customText,
|
||||
}))
|
||||
.with(
|
||||
FieldType.NUMBER,
|
||||
FieldType.RADIO,
|
||||
FieldType.CHECKBOX,
|
||||
FieldType.DROPDOWN,
|
||||
(type) => ({
|
||||
type,
|
||||
data: updatedField.customText,
|
||||
}),
|
||||
)
|
||||
.exhaustive(),
|
||||
fieldSecurity: derivedRecipientActionAuth
|
||||
? {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType, Team } from '@documenso/prisma/client';
|
||||
|
||||
@ -18,6 +19,7 @@ export type UpdateFieldOptions = {
|
||||
pageWidth?: number;
|
||||
pageHeight?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
fieldMeta?: FieldMeta;
|
||||
};
|
||||
|
||||
export const updateField = async ({
|
||||
@ -33,6 +35,7 @@ export const updateField = async ({
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata,
|
||||
fieldMeta,
|
||||
}: UpdateFieldOptions) => {
|
||||
const oldField = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
@ -71,6 +74,7 @@ export const updateField = async ({
|
||||
positionY: pageY,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
fieldMeta,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { PDFDocument, RotationTypes, degrees, radiansToDegrees } from 'pdf-lib';
|
||||
import { match } from 'ts-pattern';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import {
|
||||
DEFAULT_HANDWRITING_FONT_SIZE,
|
||||
@ -13,6 +13,8 @@ import { FieldType } from '@documenso/prisma/client';
|
||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
|
||||
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '../../types/field-meta';
|
||||
|
||||
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||
const fontCaveat = await fetch(process.env.FONT_CAVEAT_URI).then(async (res) =>
|
||||
res.arrayBuffer(),
|
||||
@ -77,86 +79,163 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
||||
await pdf.embedFont(fontCaveat);
|
||||
}
|
||||
|
||||
const isInsertingImage =
|
||||
isSignatureField && typeof field.Signature?.signatureImageAsBase64 === 'string';
|
||||
await match(field)
|
||||
.with(
|
||||
{
|
||||
type: P.union(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE),
|
||||
Signature: { signatureImageAsBase64: P.string },
|
||||
},
|
||||
async (field) => {
|
||||
const image = await pdf.embedPng(field.Signature?.signatureImageAsBase64 ?? '');
|
||||
|
||||
if (isSignatureField && isInsertingImage) {
|
||||
const image = await pdf.embedPng(field.Signature?.signatureImageAsBase64 ?? '');
|
||||
let imageWidth = image.width;
|
||||
let imageHeight = image.height;
|
||||
|
||||
let imageWidth = image.width;
|
||||
let imageHeight = image.height;
|
||||
const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1);
|
||||
|
||||
const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1);
|
||||
imageWidth = imageWidth * scalingFactor;
|
||||
imageHeight = imageHeight * scalingFactor;
|
||||
|
||||
imageWidth = imageWidth * scalingFactor;
|
||||
imageHeight = imageHeight * scalingFactor;
|
||||
let imageX = fieldX + (fieldWidth - imageWidth) / 2;
|
||||
let imageY = fieldY + (fieldHeight - imageHeight) / 2;
|
||||
|
||||
let imageX = fieldX + (fieldWidth - imageWidth) / 2;
|
||||
let imageY = fieldY + (fieldHeight - imageHeight) / 2;
|
||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||
imageY = pageHeight - imageY - imageHeight;
|
||||
|
||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||
imageY = pageHeight - imageY - imageHeight;
|
||||
if (pageRotationInDegrees !== 0) {
|
||||
const adjustedPosition = adjustPositionForRotation(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
imageX,
|
||||
imageY,
|
||||
pageRotationInDegrees,
|
||||
);
|
||||
|
||||
if (pageRotationInDegrees !== 0) {
|
||||
const adjustedPosition = adjustPositionForRotation(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
imageX,
|
||||
imageY,
|
||||
pageRotationInDegrees,
|
||||
);
|
||||
imageX = adjustedPosition.xPos;
|
||||
imageY = adjustedPosition.yPos;
|
||||
}
|
||||
|
||||
imageX = adjustedPosition.xPos;
|
||||
imageY = adjustedPosition.yPos;
|
||||
}
|
||||
page.drawImage(image, {
|
||||
x: imageX,
|
||||
y: imageY,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
});
|
||||
},
|
||||
)
|
||||
.with({ type: FieldType.CHECKBOX }, (field) => {
|
||||
const meta = ZCheckboxFieldMeta.safeParse(field.fieldMeta);
|
||||
|
||||
page.drawImage(image, {
|
||||
x: imageX,
|
||||
y: imageY,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
if (!meta.success) {
|
||||
console.error(meta.error);
|
||||
|
||||
throw new Error('Invalid checkbox field meta');
|
||||
}
|
||||
|
||||
const selected = field.customText.split(',');
|
||||
|
||||
for (const [index, item] of (meta.data.values ?? []).entries()) {
|
||||
const offsetY = index * 16;
|
||||
|
||||
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
|
||||
|
||||
if (selected.includes(item.value)) {
|
||||
checkbox.check();
|
||||
}
|
||||
|
||||
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
|
||||
x: fieldX + 16,
|
||||
y: pageHeight - (fieldY + offsetY),
|
||||
size: 12,
|
||||
font,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
});
|
||||
|
||||
checkbox.addToPage(page, {
|
||||
x: fieldX,
|
||||
y: pageHeight - (fieldY + offsetY),
|
||||
height: 8,
|
||||
width: 8,
|
||||
});
|
||||
}
|
||||
})
|
||||
.with({ type: FieldType.RADIO }, (field) => {
|
||||
const meta = ZRadioFieldMeta.safeParse(field.fieldMeta);
|
||||
|
||||
if (!meta.success) {
|
||||
console.error(meta.error);
|
||||
|
||||
throw new Error('Invalid radio field meta');
|
||||
}
|
||||
|
||||
const selected = field.customText.split(',');
|
||||
|
||||
for (const [index, item] of (meta.data.values ?? []).entries()) {
|
||||
const offsetY = index * 16;
|
||||
|
||||
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
|
||||
|
||||
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
|
||||
x: fieldX + 16,
|
||||
y: pageHeight - (fieldY + offsetY),
|
||||
size: 12,
|
||||
font,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
});
|
||||
|
||||
radio.addOptionToPage(item.value, page, {
|
||||
x: fieldX,
|
||||
y: pageHeight - (fieldY + offsetY),
|
||||
height: 8,
|
||||
width: 8,
|
||||
});
|
||||
|
||||
if (selected.includes(item.value)) {
|
||||
radio.select(item.value);
|
||||
}
|
||||
}
|
||||
})
|
||||
.otherwise((field) => {
|
||||
const longestLineInTextForWidth = field.customText
|
||||
.split('\n')
|
||||
.sort((a, b) => b.length - a.length)[0];
|
||||
|
||||
let textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
|
||||
const textHeight = font.heightAtSize(fontSize);
|
||||
|
||||
const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1);
|
||||
|
||||
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
|
||||
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
|
||||
|
||||
let textX = fieldX + (fieldWidth - textWidth) / 2;
|
||||
let textY = fieldY + (fieldHeight - textHeight) / 2;
|
||||
|
||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||
textY = pageHeight - textY - textHeight;
|
||||
|
||||
if (pageRotationInDegrees !== 0) {
|
||||
const adjustedPosition = adjustPositionForRotation(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
textX,
|
||||
textY,
|
||||
pageRotationInDegrees,
|
||||
);
|
||||
|
||||
textX = adjustedPosition.xPos;
|
||||
textY = adjustedPosition.yPos;
|
||||
}
|
||||
|
||||
page.drawText(field.customText, {
|
||||
x: textX,
|
||||
y: textY,
|
||||
size: fontSize,
|
||||
font,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const longestLineInTextForWidth = field.customText
|
||||
.split('\n')
|
||||
.sort((a, b) => b.length - a.length)[0];
|
||||
|
||||
let textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
|
||||
const textHeight = font.heightAtSize(fontSize);
|
||||
|
||||
const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1);
|
||||
|
||||
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
|
||||
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
|
||||
|
||||
let textX = fieldX + (fieldWidth - textWidth) / 2;
|
||||
let textY = fieldY + (fieldHeight - textHeight) / 2;
|
||||
|
||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||
textY = pageHeight - textY - textHeight;
|
||||
|
||||
if (pageRotationInDegrees !== 0) {
|
||||
const adjustedPosition = adjustPositionForRotation(
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
textX,
|
||||
textY,
|
||||
pageRotationInDegrees,
|
||||
);
|
||||
|
||||
textX = adjustedPosition.xPos;
|
||||
textY = adjustedPosition.yPos;
|
||||
}
|
||||
|
||||
page.drawText(field.customText, {
|
||||
x: textX,
|
||||
y: textY,
|
||||
size: fontSize,
|
||||
font,
|
||||
rotate: degrees(pageRotationInDegrees),
|
||||
});
|
||||
}
|
||||
|
||||
return pdf;
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
Prisma,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
@ -26,6 +27,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
|
||||
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
||||
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
@ -296,12 +298,16 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
await tx.field.createMany({
|
||||
data: nonDirectRecipientFieldsToCreate,
|
||||
data: nonDirectRecipientFieldsToCreate.map((field) => ({
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
})),
|
||||
});
|
||||
|
||||
// Create the direct recipient and their non signature fields.
|
||||
@ -331,6 +337,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
height: templateField.height,
|
||||
customText,
|
||||
inserted: true,
|
||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||
})),
|
||||
},
|
||||
},
|
||||
@ -361,6 +368,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
height: templateField.height,
|
||||
customText: '',
|
||||
inserted: true,
|
||||
fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
|
||||
Signature: {
|
||||
create: {
|
||||
recipientId: createdDirectRecipient.id,
|
||||
@ -454,10 +462,20 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
data:
|
||||
field.Signature?.signatureImageAsBase64 || field.Signature?.typedSignature || '',
|
||||
}))
|
||||
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, FieldType.TEXT, (type) => ({
|
||||
type,
|
||||
data: field.customText,
|
||||
}))
|
||||
.with(
|
||||
FieldType.DATE,
|
||||
FieldType.EMAIL,
|
||||
FieldType.NAME,
|
||||
FieldType.TEXT,
|
||||
FieldType.NUMBER,
|
||||
FieldType.CHECKBOX,
|
||||
FieldType.DROPDOWN,
|
||||
FieldType.RADIO,
|
||||
(type) => ({
|
||||
type,
|
||||
data: field.customText,
|
||||
}),
|
||||
)
|
||||
.exhaustive(),
|
||||
fieldSecurity: derivedRecipientActionAuth
|
||||
? {
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
|
||||
import { ZFieldMetaSchema } from '../../types/field-meta';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import {
|
||||
@ -225,12 +226,16 @@ export const createDocumentFromTemplate = async ({
|
||||
height: field.height,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: field.fieldMeta,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
await tx.field.createMany({
|
||||
data: fieldsToCreate,
|
||||
data: fieldsToCreate.map((field) => ({
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
})),
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
|
||||
@ -253,6 +253,22 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
|
||||
type: z.union([z.literal(FieldType.SIGNATURE), z.literal(FieldType.FREE_SIGNATURE)]),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.RADIO),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.CHECKBOX),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.DROPDOWN),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.NUMBER),
|
||||
data: z.string(),
|
||||
}),
|
||||
]),
|
||||
fieldSecurity: z.preprocess(
|
||||
(input) => {
|
||||
|
||||
80
packages/lib/types/field-meta.ts
Normal file
80
packages/lib/types/field-meta.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZBaseFieldMeta = z.object({
|
||||
label: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
required: z.boolean().optional(),
|
||||
readOnly: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TBaseFieldMeta = z.infer<typeof ZBaseFieldMeta>;
|
||||
|
||||
export const ZTextFieldMeta = ZBaseFieldMeta.extend({
|
||||
type: z.literal('text').default('text'),
|
||||
text: z.string().optional(),
|
||||
characterLimit: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
|
||||
|
||||
export const ZNumberFieldMeta = ZBaseFieldMeta.extend({
|
||||
type: z.literal('number').default('number'),
|
||||
numberFormat: z.string().optional(),
|
||||
value: z.string().optional(),
|
||||
minValue: z.number().optional(),
|
||||
maxValue: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TNumberFieldMeta = z.infer<typeof ZNumberFieldMeta>;
|
||||
|
||||
export const ZRadioFieldMeta = ZBaseFieldMeta.extend({
|
||||
type: z.literal('radio').default('radio'),
|
||||
values: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
checked: z.boolean(),
|
||||
value: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TRadioFieldMeta = z.infer<typeof ZRadioFieldMeta>;
|
||||
|
||||
export const ZCheckboxFieldMeta = ZBaseFieldMeta.extend({
|
||||
type: z.literal('checkbox').default('checkbox'),
|
||||
values: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
checked: z.boolean(),
|
||||
value: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
validationRule: z.string().optional(),
|
||||
validationLength: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TCheckboxFieldMeta = z.infer<typeof ZCheckboxFieldMeta>;
|
||||
|
||||
export const ZDropdownFieldMeta = ZBaseFieldMeta.extend({
|
||||
type: z.literal('dropdown').default('dropdown'),
|
||||
values: z.array(z.object({ value: z.string() })).optional(),
|
||||
defaultValue: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TDropdownFieldMeta = z.infer<typeof ZDropdownFieldMeta>;
|
||||
|
||||
export const ZFieldMetaSchema = z
|
||||
.union([
|
||||
ZTextFieldMeta,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
])
|
||||
.optional();
|
||||
|
||||
export type TFieldMetaSchema = z.infer<typeof ZFieldMetaSchema>;
|
||||
@ -1,4 +1,4 @@
|
||||
import { Field } from '@documenso/prisma/client';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
|
||||
/**
|
||||
* Sort the fields by the Y position on the document.
|
||||
|
||||
Reference in New Issue
Block a user