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:
Catalin Pit
2024-07-18 16:45:44 +03:00
committed by GitHub
parent a3ee732a9b
commit 7b5c57e8af
74 changed files with 5234 additions and 829 deletions

View File

@ -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,
},
},
},
},
],
},
},
});

View File

@ -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,

View File

@ -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) {

View File

@ -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
? {

View File

@ -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,