feat: add envelope editor

This commit is contained in:
David Nguyen
2025-10-12 23:35:54 +11:00
parent bf89bc781b
commit 0da8e7dbc6
307 changed files with 24657 additions and 3681 deletions

View File

@ -1,8 +1,14 @@
import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
import { z } from 'zod';
import { VALID_DATE_FORMAT_VALUES } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import { ZDocumentEmailSettingsSchema } from './document-email';
/**
* The full document response schema.
*
@ -15,9 +21,7 @@ export const ZDocumentMetaSchema = DocumentMetaSchema.pick({
subject: true,
message: true,
timezone: true,
password: true,
dateFormat: true,
documentId: true,
redirectUrl: true,
typedSignatureEnabled: true,
uploadSignatureEnabled: true,
@ -51,3 +55,87 @@ export const ZDocumentSignatureSettingsSchema = z
);
export type TDocumentSignatureSettings = z.infer<typeof ZDocumentSignatureSettingsSchema>;
export const ZDocumentMetaTimezoneSchema = z
.string()
.describe(
'The timezone to use for date fields and signing the document. Example Etc/UTC, Australia/Melbourne',
);
export type TDocumentMetaTimezone = z.infer<typeof ZDocumentMetaTimezoneSchema>;
export const ZDocumentMetaDateFormatSchema = z
.enum(VALID_DATE_FORMAT_VALUES)
.describe('The date format to use for date fields and signing the document.');
export type TDocumentMetaDateFormat = z.infer<typeof ZDocumentMetaDateFormatSchema>;
export const ZDocumentMetaRedirectUrlSchema = z
.string()
.describe('The URL to which the recipient should be redirected after signing the document.')
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message: 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
});
export const ZDocumentMetaLanguageSchema = z
.enum(SUPPORTED_LANGUAGE_CODES)
.describe('The language to use for email communications with recipients.');
export const ZDocumentMetaSubjectSchema = z
.string()
.max(254)
.describe('The subject of the email that will be sent to the recipients.');
export const ZDocumentMetaMessageSchema = z
.string()
.max(5000)
.describe('The message of the email that will be sent to the recipients.');
export const ZDocumentMetaDistributionMethodSchema = z
.nativeEnum(DocumentDistributionMethod)
.describe('The distribution method to use when sending the document to the recipients.');
export const ZDocumentMetaTypedSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using a typed signature.');
export const ZDocumentMetaDrawSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using a draw signature.');
export const ZDocumentMetaUploadSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using an uploaded signature.');
/**
* Note: Any updates to this will cause public API changes. You will need to update
* all corresponding areas where this is used (some places that use this needs to pass
* it through to another function).
*/
export const ZDocumentMetaCreateSchema = z.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
emailId: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
});
export type TDocumentMetaCreate = z.infer<typeof ZDocumentMetaCreateSchema>;
/**
* Note: This is the same as the create schema for now since there are
* no nullable values. Once there is we will need to update this properly.
*/
export const ZDocumentMetaUpdateSchema = ZDocumentMetaCreateSchema;
export type TDocumentMetaUpdate = z.infer<typeof ZDocumentMetaUpdateSchema>;

View File

@ -32,9 +32,13 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
teamId: true,
folderId: true,
}).extend({
// Which "Template" the document was created from. Legacy field for backwards compatibility.
// The actual field is now called `createdFromDocumentId`.
templateId: z.number().nullish(),
envelopeId: z.string(),
// Which "Template" the document was created from.
templateId: z
.number()
.nullish()
.describe('The ID of the template that the document was created from, if any.'),
// Todo: Maybe we want to alter this a bit since this returns a lot of data.
documentData: DocumentDataSchema.pick({
@ -42,6 +46,8 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
id: true,
data: true,
initialData: true,
}).extend({
envelopeItemId: z.string(),
}),
documentMeta: DocumentMetaSchema.pick({
signingOrder: true,
@ -100,9 +106,13 @@ export const ZDocumentLiteSchema = LegacyDocumentSchema.pick({
folderId: true,
useLegacyFieldInsertion: true,
}).extend({
// Which "Template" the document was created from. Legacy field for backwards compatibility.
// The actual field is now called `createdFromDocumentId`.
templateId: z.number().nullish(),
envelopeId: z.string(),
// Which "Template" the document was created from.
templateId: z
.number()
.nullish()
.describe('The ID of the template that the document was created from, if any.'),
});
export type TDocumentLite = z.infer<typeof ZDocumentLiteSchema>;
@ -128,9 +138,13 @@ export const ZDocumentManySchema = LegacyDocumentSchema.pick({
folderId: true,
useLegacyFieldInsertion: true,
}).extend({
// Which "Template" the document was created from. Legacy field for backwards compatibility.
// The actual field is now called `createdFromDocumentId`.
templateId: z.number().nullish(),
envelopeId: z.string(),
// Which "Template" the document was created from.
templateId: z
.number()
.nullish()
.describe('The ID of the template that the document was created from, if any.'),
user: UserSchema.pick({
id: true,

View File

@ -0,0 +1,138 @@
import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import { EnvelopeItemSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { EnvelopeSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import TemplateDirectLinkSchema from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
import { ZFieldSchema } from './field';
import { ZRecipientLiteSchema } from './recipient';
/**
* The full envelope response schema.
*
* Mainly used for returning a single envelope from the API.
*/
export const ZEnvelopeSchema = EnvelopeSchema.pick({
internalVersion: true,
type: true,
status: true,
source: true,
visibility: true,
templateType: true,
id: true,
secondaryId: true,
externalId: true,
createdAt: true,
updatedAt: true,
completedAt: true,
deletedAt: true,
title: true,
authOptions: true,
formValues: true,
publicTitle: true,
publicDescription: true,
userId: true,
teamId: true,
folderId: true,
}).extend({
templateId: z
.number()
.nullish()
.describe('The ID of the template that the document was created from, if any.'),
documentMeta: DocumentMetaSchema.pick({
signingOrder: true,
distributionMethod: true,
id: true,
subject: true,
message: true,
timezone: true,
dateFormat: true,
redirectUrl: true,
typedSignatureEnabled: true,
uploadSignatureEnabled: true,
drawSignatureEnabled: true,
allowDictateNextSigner: true,
language: true,
emailSettings: true,
emailId: true,
emailReplyTo: true,
}),
recipients: ZRecipientLiteSchema.omit({
documentId: true,
templateId: true,
}).array(),
fields: ZFieldSchema.omit({
documentId: true,
templateId: true,
}).array(),
envelopeItems: EnvelopeItemSchema.pick({
id: true,
title: true,
documentDataId: true,
order: true,
})
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true, // Todo: Envelopes - Maybe this hide this.
}),
})
.array(),
directLink: TemplateDirectLinkSchema.pick({
directTemplateRecipientId: true,
enabled: true,
id: true,
token: true,
}).nullable(),
team: TeamSchema.pick({
id: true,
url: true,
}),
user: z.object({
id: z.number(),
name: z.string(),
email: z.string(),
}),
});
export type TEnvelope = z.infer<typeof ZEnvelopeSchema>;
/**
* A lite version of the envelope response schema without relations.
*/
export const ZEnvelopeLiteSchema = EnvelopeSchema.pick({
internalVersion: true,
type: true,
status: true,
source: true,
visibility: true,
templateType: true,
id: true,
secondaryId: true,
externalId: true,
createdAt: true,
updatedAt: true,
completedAt: true,
deletedAt: true,
title: true,
authOptions: true,
formValues: true,
publicTitle: true,
publicDescription: true,
userId: true,
teamId: true,
folderId: true,
});
export type TEnvelopeLite = z.infer<typeof ZEnvelopeLiteSchema>;
/**
* A version of the envelope response schema when returning multiple envelopes at once from a single API endpoint.
*/
// export const ZEnvelopeManySchema = X
// export type TEnvelopeMany = z.infer<typeof ZEnvelopeManySchema>;

View File

@ -1,6 +1,8 @@
import { FieldType } from '@prisma/client';
import { z } from 'zod';
export const DEFAULT_FIELD_FONT_SIZE = 14;
export const ZBaseFieldMeta = z.object({
label: z.string().optional(),
placeholder: z.string().optional(),
@ -58,10 +60,10 @@ export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
export const ZNumberFieldMeta = ZBaseFieldMeta.extend({
type: z.literal('number'),
numberFormat: z.string().optional(),
numberFormat: z.string().nullish(),
value: z.string().optional(),
minValue: z.number().optional(),
maxValue: z.number().optional(),
minValue: z.coerce.number().nullish(),
maxValue: z.coerce.number().nullish(),
fontSize: z.number().min(8).max(96).optional(),
textAlign: ZFieldTextAlignSchema.optional(),
});
@ -226,3 +228,86 @@ export const ZFieldAndMetaSchema = z.discriminatedUnion('type', [
]);
export type TFieldAndMeta = z.infer<typeof ZFieldAndMetaSchema>;
export const FIELD_DATE_META_DEFAULT_VALUES: TDateFieldMeta = {
type: 'date',
fontSize: 14,
textAlign: 'left',
};
export const FIELD_TEXT_META_DEFAULT_VALUES: TTextFieldMeta = {
type: 'text',
fontSize: 14,
textAlign: 'left',
label: '',
placeholder: '',
text: '',
required: false,
readOnly: false,
};
export const FIELD_NUMBER_META_DEFAULT_VALUES: TNumberFieldMeta = {
type: 'number',
fontSize: 14,
textAlign: 'left',
label: '',
placeholder: '',
required: false,
readOnly: false,
};
export const FIELD_INITIALS_META_DEFAULT_VALUES: TInitialsFieldMeta = {
type: 'initials',
fontSize: 14,
textAlign: 'left',
};
export const FIELD_NAME_META_DEFAULT_VALUES: TNameFieldMeta = {
type: 'name',
fontSize: 14,
textAlign: 'left',
};
export const FIELD_EMAIL_META_DEFAULT_VALUES: TEmailFieldMeta = {
type: 'email',
fontSize: 14,
textAlign: 'left',
};
export const FIELD_RADIO_META_DEFAULT_VALUES: TRadioFieldMeta = {
type: 'radio',
values: [{ id: 1, checked: false, value: '' }],
required: false,
readOnly: false,
};
export const FIELD_CHECKBOX_META_DEFAULT_VALUES: TCheckboxFieldMeta = {
type: 'checkbox',
values: [{ id: 1, checked: false, value: '' }],
validationRule: '',
validationLength: 0,
required: false,
readOnly: false,
direction: 'vertical',
};
export const FIELD_DROPDOWN_META_DEFAULT_VALUES: TDropdownFieldMeta = {
type: 'dropdown',
values: [{ value: 'Option 1' }],
defaultValue: '',
required: false,
readOnly: false,
};
export const FIELD_META_DEFAULT_VALUES: Record<FieldType, TFieldMetaSchema> = {
[FieldType.SIGNATURE]: undefined,
[FieldType.FREE_SIGNATURE]: undefined,
[FieldType.INITIALS]: FIELD_INITIALS_META_DEFAULT_VALUES,
[FieldType.NAME]: FIELD_NAME_META_DEFAULT_VALUES,
[FieldType.EMAIL]: FIELD_EMAIL_META_DEFAULT_VALUES,
[FieldType.DATE]: FIELD_DATE_META_DEFAULT_VALUES,
[FieldType.TEXT]: FIELD_TEXT_META_DEFAULT_VALUES,
[FieldType.NUMBER]: FIELD_NUMBER_META_DEFAULT_VALUES,
[FieldType.RADIO]: FIELD_RADIO_META_DEFAULT_VALUES,
[FieldType.CHECKBOX]: FIELD_CHECKBOX_META_DEFAULT_VALUES,
[FieldType.DROPDOWN]: FIELD_DROPDOWN_META_DEFAULT_VALUES,
} as const;

View File

@ -1,7 +1,20 @@
import { FieldType, Prisma } from '@prisma/client';
import { z } from 'zod';
import { FieldSchema } from '@documenso/prisma/generated/zod/modelSchema/FieldSchema';
import {
ZCheckboxFieldMeta,
ZDateFieldMeta,
ZDropdownFieldMeta,
ZEmailFieldMeta,
ZInitialsFieldMeta,
ZNameFieldMeta,
ZNumberFieldMeta,
ZRadioFieldMeta,
ZTextFieldMeta,
} from './field-meta';
/**
* The full field response schema.
*
@ -30,7 +43,7 @@ export const ZFieldSchema = FieldSchema.pick({
inserted: true,
fieldMeta: true,
}).extend({
// Todo: Migration - Backwards compatibility.
// Backwards compatibility.
documentId: z.number().nullish(),
templateId: z.number().nullish(),
});
@ -53,3 +66,110 @@ export const ZFieldPageYSchema = z
export const ZFieldWidthSchema = z.number().min(1).describe('The width of the field.');
export const ZFieldHeightSchema = z.number().min(1).describe('The height of the field.');
// ---------------------------------------------
// Todo: Envelopes - dunno man
const PrismaDecimalSchema = z.preprocess(
(val) => (typeof val === 'string' ? new Prisma.Decimal(val) : val),
z.instanceof(Prisma.Decimal, { message: 'Must be a Decimal' }),
);
export const BaseFieldSchemaUsingNumbers = ZFieldSchema.extend({
positionX: PrismaDecimalSchema,
positionY: PrismaDecimalSchema,
width: PrismaDecimalSchema,
height: PrismaDecimalSchema,
});
export const ZFieldTextSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.TEXT),
fieldMeta: ZTextFieldMeta,
});
export type TFieldText = z.infer<typeof ZFieldTextSchema>;
export const ZFieldSignatureSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.SIGNATURE),
fieldMeta: z.literal(null),
});
export type TFieldSignature = z.infer<typeof ZFieldSignatureSchema>;
export const ZFieldFreeSignatureSchema = ZFieldSignatureSchema;
export type TFieldFreeSignature = z.infer<typeof ZFieldFreeSignatureSchema>;
export const ZFieldInitialsSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.INITIALS),
fieldMeta: ZInitialsFieldMeta,
});
export type TFieldInitials = z.infer<typeof ZFieldInitialsSchema>;
export const ZFieldNameSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.NAME),
fieldMeta: ZNameFieldMeta,
});
export type TFieldName = z.infer<typeof ZFieldNameSchema>;
export const ZFieldEmailSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.EMAIL),
fieldMeta: ZEmailFieldMeta,
});
export type TFieldEmail = z.infer<typeof ZFieldEmailSchema>;
export const ZFieldDateSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.DATE),
fieldMeta: ZDateFieldMeta,
});
export type TFieldDate = z.infer<typeof ZFieldDateSchema>;
export const ZFieldNumberSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.NUMBER),
fieldMeta: ZNumberFieldMeta,
});
export type TFieldNumber = z.infer<typeof ZFieldNumberSchema>;
export const ZFieldRadioSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.RADIO),
fieldMeta: ZRadioFieldMeta,
});
export type TFieldRadio = z.infer<typeof ZFieldRadioSchema>;
export const ZFieldCheckboxSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.CHECKBOX),
fieldMeta: ZCheckboxFieldMeta,
});
export type TFieldCheckbox = z.infer<typeof ZFieldCheckboxSchema>;
export const ZFieldDropdownSchema = BaseFieldSchemaUsingNumbers.extend({
type: z.literal(FieldType.DROPDOWN),
fieldMeta: ZDropdownFieldMeta,
});
export type TFieldDropdown = z.infer<typeof ZFieldDropdownSchema>;
/**
* The full field schema which will enforce all types and meta fields.
*/
export const ZFullFieldSchema = z.discriminatedUnion('type', [
ZFieldTextSchema,
ZFieldSignatureSchema,
ZFieldInitialsSchema,
ZFieldNameSchema,
ZFieldEmailSchema,
ZFieldDateSchema,
ZFieldNumberSchema,
ZFieldRadioSchema,
ZFieldCheckboxSchema,
ZFieldDropdownSchema,
]);
export type TFullFieldSchema = z.infer<typeof ZFullFieldSchema>;

View File

@ -30,7 +30,7 @@ export const ZRecipientSchema = RecipientSchema.pick({
}).extend({
fields: ZFieldSchema.array(),
// Todo: Migration - Backwards compatibility.
// Backwards compatibility.
documentId: z.number().nullish(),
templateId: z.number().nullish(),
});
@ -55,7 +55,7 @@ export const ZRecipientLiteSchema = RecipientSchema.pick({
signingOrder: true,
rejectionReason: true,
}).extend({
// Todo: Migration - Backwards compatibility.
// Backwards compatibility.
documentId: z.number().nullish(),
templateId: z.number().nullish(),
});
@ -91,7 +91,7 @@ export const ZRecipientManySchema = RecipientSchema.pick({
url: true,
}).nullable(),
// Todo: Migration - Backwards compatibility.
// Backwards compatibility.
documentId: z.number().nullish(),
templateId: z.number().nullish(),
});

View File

@ -1,4 +1,4 @@
import type { z } from 'zod';
import { z } from 'zod';
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
@ -31,12 +31,16 @@ export const ZTemplateSchema = TemplateSchema.pick({
publicDescription: true,
folderId: true,
}).extend({
envelopeId: z.string(),
// Todo: Maybe we want to alter this a bit since this returns a lot of data.
templateDocumentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}).extend({
envelopeItemId: z.string(),
}),
templateMeta: DocumentMetaSchema.pick({
id: true,
@ -98,6 +102,8 @@ export const ZTemplateLiteSchema = TemplateSchema.pick({
publicDescription: true,
folderId: true,
useLegacyFieldInsertion: true,
}).extend({
envelopeId: z.string(),
});
export type TTemplateLite = z.infer<typeof ZTemplateLiteSchema>;
@ -121,6 +127,7 @@ export const ZTemplateManySchema = TemplateSchema.pick({
folderId: true,
useLegacyFieldInsertion: true,
}).extend({
envelopeId: z.string(),
team: TeamSchema.pick({
id: true,
url: true,