feat: polish envelopes (#2090)

## Description

The rest of the owl
This commit is contained in:
David Nguyen
2025-10-24 16:22:06 +11:00
committed by GitHub
parent 88836404d1
commit 03eb6af69a
141 changed files with 5171 additions and 2402 deletions
+2
View File
@@ -76,6 +76,7 @@ export const mapEnvelopeToDocumentLite = (envelope: Envelope): TDocumentLite =>
return {
id: documentId, // Use legacy ID.
envelopeId: envelope.id,
internalVersion: envelope.internalVersion,
visibility: envelope.visibility,
status: envelope.status,
source: envelope.source,
@@ -115,6 +116,7 @@ export const mapEnvelopesToDocumentMany = (
return {
id: legacyDocumentId, // Use legacy ID.
envelopeId: envelope.id,
internalVersion: envelope.internalVersion,
visibility: envelope.visibility,
status: envelope.status,
source: envelope.source,
+263
View File
@@ -0,0 +1,263 @@
import type { Field } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { DateTime } from 'luxon';
import { P, match } from 'ts-pattern';
import { z } from 'zod';
import { validateCheckboxLength } 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 { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TDocumentMeta } from '@documenso/lib/types/document-meta';
import {
ZCheckboxFieldMeta,
ZDropdownFieldMeta,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { toCheckboxCustomText, toRadioCustomText } from '@documenso/lib/utils/fields';
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
export type ExtractFieldInsertionValuesOptions = {
fieldValue: TSignEnvelopeFieldValue;
field: Field;
documentMeta: Pick<TDocumentMeta, 'timezone' | 'dateFormat' | 'typedSignatureEnabled'>;
};
export const extractFieldInsertionValues = ({
fieldValue,
field,
documentMeta,
}: ExtractFieldInsertionValuesOptions): { customText: string; inserted: boolean } => {
return match(fieldValue)
.with({ type: FieldType.EMAIL }, (fieldValue) => {
const parsedEmailValue = z.string().email().nullable().safeParse(fieldValue.value);
if (!parsedEmailValue.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid email',
});
}
if (parsedEmailValue.data === null) {
return {
customText: '',
inserted: false,
};
}
return {
customText: parsedEmailValue.data,
inserted: true,
};
})
.with({ type: P.union(FieldType.NAME, FieldType.INITIALS) }, (fieldValue) => {
const parsedGenericStringValue = z.string().min(1).nullable().safeParse(fieldValue.value);
if (!parsedGenericStringValue.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Value is required',
});
}
if (parsedGenericStringValue.data === null) {
return {
customText: '',
inserted: false,
};
}
return {
customText: parsedGenericStringValue.data,
inserted: true,
};
})
.with({ type: FieldType.DATE }, (fieldValue) => {
if (!fieldValue.value) {
return {
customText: '',
inserted: false,
};
}
return {
customText: DateTime.now()
.setZone(documentMeta.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE)
.toFormat(documentMeta.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT),
inserted: true,
};
})
.with({ type: FieldType.NUMBER }, (fieldValue) => {
if (!fieldValue.value) {
return {
customText: '',
inserted: false,
};
}
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true);
// Todo
if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid number',
});
}
return {
customText: fieldValue.value.toString(),
inserted: true,
};
})
.with({ type: FieldType.TEXT }, (fieldValue) => {
if (fieldValue.value === null) {
return {
customText: '',
inserted: false,
};
}
const parsedTextFieldMeta = ZTextFieldMeta.parse(field.fieldMeta);
const errors = validateTextField(fieldValue.value, parsedTextFieldMeta, true);
// Todo
if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid email',
});
}
return {
customText: fieldValue.value,
inserted: true,
};
})
.with({ type: FieldType.RADIO }, (fieldValue) => {
if (fieldValue.value === null) {
return {
customText: '',
inserted: false,
};
}
const parsedRadioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
const radioFieldValues = parsedRadioFieldParsedMeta.values || [];
if (!radioFieldValues[fieldValue.value]) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid radio value',
});
}
return {
customText: toRadioCustomText(fieldValue.value),
inserted: true,
};
})
.with({ type: FieldType.CHECKBOX }, (fieldValue) => {
if (fieldValue.value === null || fieldValue.value.length === 0) {
return {
customText: '',
inserted: false,
};
}
const parsedCheckboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
const checkboxFieldValues = parsedCheckboxFieldParsedMeta.values || [];
const { value } = fieldValue;
const selectedValues = value.map((valueIndex) => checkboxFieldValues[valueIndex]);
if (selectedValues.some((value) => !value)) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid checkbox values',
});
}
const { validationRule, validationLength } = parsedCheckboxFieldParsedMeta;
if (validationRule && validationLength) {
const checkboxValidationRule = checkboxValidationSigns.find(
(sign) => sign.label === validationRule,
);
// Todo: Envelopes - Test this.
if (checkboxValidationRule) {
const isValid = validateCheckboxLength(
selectedValues.length,
checkboxValidationRule.value,
validationLength,
);
if (!isValid) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Checkbox values failed length validation',
});
}
} else {
// Should throw an error, but we don't want to throw configuration errors during signing.
// Todo: Logging.
}
}
return {
customText: toCheckboxCustomText(fieldValue.value),
inserted: true,
};
})
.with({ type: FieldType.DROPDOWN }, (fieldValue) => {
if (fieldValue.value === null) {
return {
customText: '',
inserted: false,
};
}
const parsedDropdownFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
const errors = validateDropdownField(fieldValue.value, parsedDropdownFieldMeta, true);
// Todo: Envelopes
if (errors.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid dropdown value',
});
}
return {
customText: fieldValue.value,
inserted: true,
};
})
.with({ type: FieldType.SIGNATURE }, (fieldValue) => {
const { value } = fieldValue;
if (!value) {
return {
customText: '',
inserted: false,
};
}
const isBase64 = isBase64Image(value);
if (documentMeta.typedSignatureEnabled === false && !isBase64) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Typed signatures are not allowed. Please draw your signature',
});
}
return {
customText: '',
inserted: true,
};
})
.exhaustive();
};
+35 -1
View File
@@ -1,4 +1,6 @@
import { type Envelope, type Field } from '@prisma/client';
import type { I18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { type Envelope, type Field, FieldType } from '@prisma/client';
import { extractLegacyIds } from '../universal/id';
@@ -77,3 +79,35 @@ export const mapFieldToLegacyField = (
...legacyId,
};
};
export const parseCheckboxCustomText = (customText: string): number[] => {
return JSON.parse(customText);
};
export const toCheckboxCustomText = (checkedValues: number[]): string => {
return JSON.stringify(checkedValues);
};
export const parseRadioCustomText = (customText: string): number => {
return Number(customText);
};
export const toRadioCustomText = (value: number): string => {
return value.toString();
};
export const getClientSideFieldTranslations = ({ t }: I18n): Record<FieldType, string> => {
return {
[FieldType.TEXT]: t(msg`Text`),
[FieldType.CHECKBOX]: t(msg`Checkbox`),
[FieldType.RADIO]: t(msg`Radio`),
[FieldType.DROPDOWN]: t(msg`Dropdown`),
[FieldType.SIGNATURE]: t(msg`Signature`),
[FieldType.FREE_SIGNATURE]: t(msg`Free Signature`),
[FieldType.INITIALS]: t(msg`Initials`),
[FieldType.NAME]: t(msg`Name`),
[FieldType.NUMBER]: t(msg`Number`),
[FieldType.DATE]: t(msg`Date`),
[FieldType.EMAIL]: t(msg`Email`),
};
};
@@ -1,5 +1,7 @@
import type { SubscriptionClaim } from '@prisma/client';
import { DEFAULT_MINIMUM_ENVELOPE_ITEM_COUNT } from '@documenso/ee/server-only/limits/constants';
export const generateDefaultSubscriptionClaim = (): Omit<
SubscriptionClaim,
'id' | 'organisation' | 'createdAt' | 'updatedAt' | 'originalSubscriptionClaimId'
@@ -8,6 +10,7 @@ export const generateDefaultSubscriptionClaim = (): Omit<
name: '',
teamCount: 1,
memberCount: 1,
envelopeItemCount: DEFAULT_MINIMUM_ENVELOPE_ITEM_COUNT,
locked: false,
flags: {},
};