Merge branch 'main' into feat/prefetch-intent-navigation-links

This commit is contained in:
ephraimduncan
2026-04-20 00:20:51 +00:00
341 changed files with 12631 additions and 2208 deletions
+16
View File
@@ -571,6 +571,22 @@ export const formatDocumentAuditLogAction = (
you: msg`You deleted an envelope item with title ${data.envelopeItemTitle}`,
user: msg`${user} deleted an envelope item with title ${data.envelopeItemTitle}`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_UPDATED }, () => ({
anonymous: msg({
message: `Envelope item updated`,
context: `Audit log format`,
}),
you: msg`You updated an envelope item`,
user: msg`${user} updated an envelope item`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_PDF_REPLACED }, ({ data }) => ({
anonymous: msg({
message: `Envelope item PDF replaced`,
context: `Audit log format`,
}),
you: msg`You replaced the PDF for envelope item ${data.envelopeItemTitle}`,
user: msg`${user} replaced the PDF for envelope item ${data.envelopeItemTitle}`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRED }, ({ data }) => ({
anonymous: msg({
message: `Recipient signing window expired`,
+3
View File
@@ -66,6 +66,9 @@ export const extractDerivedDocumentMeta = (
// Envelope expiration.
envelopeExpirationPeriod:
meta.envelopeExpirationPeriod ?? settings.envelopeExpirationPeriod ?? null,
// Reminder settings.
reminderSettings: meta.reminderSettings ?? settings.reminderSettings ?? null,
} satisfies Omit<DocumentMeta, 'id'>;
};
+9
View File
@@ -67,6 +67,9 @@ export const buildEmbeddedFeatures = (
allowConfigureExpirationPeriod:
features.settings?.allowConfigureExpirationPeriod ??
DEFAULT_EMBEDDED_EDITOR_CONFIG.settings.allowConfigureExpirationPeriod,
allowConfigureReminders:
features.settings?.allowConfigureReminders ??
DEFAULT_EMBEDDED_EDITOR_CONFIG.settings.allowConfigureReminders,
allowConfigureEmailSender:
features.settings?.allowConfigureEmailSender ??
DEFAULT_EMBEDDED_EDITOR_CONFIG.settings.allowConfigureEmailSender,
@@ -88,6 +91,9 @@ export const buildEmbeddedFeatures = (
allowDuplication:
features.actions?.allowDuplication ??
DEFAULT_EMBEDDED_EDITOR_CONFIG.actions.allowDuplication,
allowSaveAsTemplate:
features.actions?.allowSaveAsTemplate ??
DEFAULT_EMBEDDED_EDITOR_CONFIG.actions.allowSaveAsTemplate,
allowDownloadPDF:
features.actions?.allowDownloadPDF ??
DEFAULT_EMBEDDED_EDITOR_CONFIG.actions.allowDownloadPDF,
@@ -110,6 +116,9 @@ export const buildEmbeddedFeatures = (
allowDelete:
features.envelopeItems?.allowDelete ??
DEFAULT_EMBEDDED_EDITOR_CONFIG.envelopeItems.allowDelete,
allowReplace:
features.envelopeItems?.allowReplace ??
DEFAULT_EMBEDDED_EDITOR_CONFIG.envelopeItems.allowReplace,
}
: null,
+2 -1
View File
@@ -21,6 +21,7 @@ import {
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { toCheckboxCustomText, toRadioCustomText } from '@documenso/lib/utils/fields';
import { zEmail } from '@documenso/lib/utils/zod';
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';
@@ -37,7 +38,7 @@ export const extractFieldInsertionValues = ({
}: ExtractFieldInsertionValuesOptions): { customText: string; inserted: boolean } => {
return match(fieldValue)
.with({ type: FieldType.EMAIL }, (fieldValue) => {
const parsedEmailValue = z.string().email().nullable().safeParse(fieldValue.value);
const parsedEmailValue = zEmail().nullable().safeParse(fieldValue.value);
if (!parsedEmailValue.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
+53 -24
View File
@@ -244,28 +244,57 @@ export const mapSecondaryIdToTemplateId = (secondaryId: string) => {
return parseInt(parsed.data.split('_')[1]);
};
export const canEnvelopeItemsBeModified = (
envelope: Pick<Envelope, 'completedAt' | 'deletedAt' | 'type' | 'status'>,
recipients: Recipient[],
) => {
if (envelope.completedAt || envelope.deletedAt || envelope.status !== DocumentStatus.DRAFT) {
return false;
}
if (envelope.type === EnvelopeType.TEMPLATE) {
return true;
}
if (
recipients.some(
(recipient) =>
recipient.role !== RecipientRole.CC &&
(recipient.signingStatus === SigningStatus.SIGNED ||
recipient.sendStatus === SendStatus.SENT),
)
) {
return false;
}
return true;
export type EnvelopeItemPermissions = {
canTitleBeChanged: boolean;
canFileBeChanged: boolean;
canOrderBeChanged: boolean;
};
export const getEnvelopeItemPermissions = (
envelope: Pick<Envelope, 'completedAt' | 'deletedAt' | 'type' | 'status'>,
recipients: Pick<Recipient, 'role' | 'signingStatus' | 'sendStatus'>[],
): EnvelopeItemPermissions => {
// Always reject completed/rejected/deleted envelopes.
if (
envelope.completedAt ||
envelope.deletedAt ||
envelope.status === DocumentStatus.REJECTED ||
envelope.status === DocumentStatus.COMPLETED
) {
return {
canTitleBeChanged: false,
canFileBeChanged: false,
canOrderBeChanged: false,
};
}
// Templates can always be modified.
if (envelope.type === EnvelopeType.TEMPLATE) {
return {
canTitleBeChanged: true,
canFileBeChanged: true,
canOrderBeChanged: true,
};
}
const hasActiveRecipients = recipients.some(
(recipient) =>
recipient.role !== RecipientRole.CC &&
(recipient.signingStatus === SigningStatus.SIGNED ||
recipient.signingStatus === SigningStatus.REJECTED ||
recipient.sendStatus === SendStatus.SENT),
);
return match(envelope.status)
.with(DocumentStatus.DRAFT, () => ({
canTitleBeChanged: true,
canFileBeChanged: true,
canOrderBeChanged: true,
}))
.with(DocumentStatus.PENDING, () => ({
canTitleBeChanged: true,
canFileBeChanged: false,
canOrderBeChanged: !hasActiveRecipients, // Only allow order changes if no active recipients.
}))
.exhaustive();
};
+3
View File
@@ -9,6 +9,7 @@ import type { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/orga
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../constants/date-formats';
import { DEFAULT_ENVELOPE_EXPIRATION_PERIOD } from '../constants/envelope-expiration';
import { DEFAULT_ENVELOPE_REMINDER_SETTINGS } from '../constants/envelope-reminder';
import {
LOWEST_ORGANISATION_ROLE,
ORGANISATION_MEMBER_ROLE_HIERARCHY,
@@ -142,6 +143,8 @@ export const generateDefaultOrganisationSettings = (): Omit<
envelopeExpirationPeriod: DEFAULT_ENVELOPE_EXPIRATION_PERIOD,
reminderSettings: DEFAULT_ENVELOPE_REMINDER_SETTINGS,
aiFeaturesEnabled: false,
};
};
+25 -8
View File
@@ -1,12 +1,13 @@
import type { Envelope } from '@prisma/client';
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
import { z } from 'zod';
import { type Field, RecipientRole, SigningStatus } from '@prisma/client';
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
import { AppError, AppErrorCode } from '../errors/app-error';
import type { TRecipientLite } from '../types/recipient';
import { extractLegacyIds } from '../universal/id';
import { zEmail } from './zod';
/**
* Roles that require fields to be assigned before a document can be distributed.
@@ -20,7 +21,7 @@ export const RECIPIENT_ROLES_THAT_REQUIRE_FIELDS = [RecipientRole.SIGNER] as con
*
* Currently only SIGNERs are validated - they must have at least one signature field.
*/
export const getRecipientsWithMissingFields = <T extends Pick<Recipient, 'id' | 'role'>>(
export const getRecipientsWithMissingFields = <T extends Pick<TRecipientLite, 'id' | 'role'>>(
recipients: T[],
fields: Pick<Field, 'type' | 'recipientId'>[],
): T[] => {
@@ -42,7 +43,10 @@ export const formatSigningLink = (token: string) => `${NEXT_PUBLIC_WEBAPP_URL()}
/**
* Whether a recipient can be modified by the document owner.
*/
export const canRecipientBeModified = (recipient: Recipient, fields: Field[]) => {
export const canRecipientBeModified = (
recipient: TRecipientLite,
fields: Pick<Field, 'recipientId' | 'inserted'>[],
) => {
if (!recipient) {
return false;
}
@@ -72,7 +76,10 @@ export const canRecipientBeModified = (recipient: Recipient, fields: Field[]) =>
* - They are not a Viewer or CCer
* - They can be modified (canRecipientBeModified)
*/
export const canRecipientFieldsBeModified = (recipient: Recipient, fields: Field[]) => {
export const canRecipientFieldsBeModified = (
recipient: TRecipientLite,
fields: Pick<Field, 'recipientId' | 'inserted'>[],
) => {
if (!canRecipientBeModified(recipient, fields)) {
return false;
}
@@ -81,7 +88,7 @@ export const canRecipientFieldsBeModified = (recipient: Recipient, fields: Field
};
export const mapRecipientToLegacyRecipient = (
recipient: Recipient,
recipient: TRecipientLite,
envelope: Pick<Envelope, 'type' | 'secondaryId'>,
) => {
const legacyId = extractLegacyIds(envelope);
@@ -92,8 +99,18 @@ export const mapRecipientToLegacyRecipient = (
};
};
export const isRecipientEmailValidForSending = (recipient: Pick<Recipient, 'email'>) => {
return z.string().email().safeParse(recipient.email).success;
export const findRecipientByEmail = <T extends { email: string }>({
recipients,
userEmail,
teamEmail,
}: {
recipients: T[];
userEmail: string;
teamEmail?: string | null;
}) => recipients.find((r) => r.email === userEmail || (teamEmail && r.email === teamEmail));
export const isRecipientEmailValidForSending = (recipient: Pick<TRecipientLite, 'email'>) => {
return zEmail().safeParse(recipient.email).success;
};
/**
+2
View File
@@ -208,6 +208,8 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
envelopeExpirationPeriod: null,
reminderSettings: null,
aiFeaturesEnabled: null,
};
};
+39
View File
@@ -0,0 +1,39 @@
import { z } from 'zod';
/**
* RFC 5322 compliant email regex.
*
* This is more permissive than Zod's built-in `.email()` validator which rejects
* valid international characters (e.g. "Søren@gmail.com").
*
* Compiled once at module level to avoid re-compilation on every validation call.
*/
const EMAIL_REGEX =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~\u{0080}-\u{FFFF}-]+@[a-zA-Z0-9\u{0080}-\u{FFFF}](?:[a-zA-Z0-9\u{0080}-\u{FFFF}-]{0,61}[a-zA-Z0-9\u{0080}-\u{FFFF}])?(?:\.[a-zA-Z0-9\u{0080}-\u{FFFF}](?:[a-zA-Z0-9\u{0080}-\u{FFFF}-]{0,61}[a-zA-Z0-9\u{0080}-\u{FFFF}])?)*$/u;
const DEFAULT_EMAIL_MESSAGE = 'Invalid email address';
/**
* Creates a Zod email schema using an RFC 5322 compliant regex.
*
* Supports international characters in the local part and domain
* (e.g. "Søren@gmail.com", "user@dömain.com").
*
* Returns a standard `ZodString` so all string methods are chainable:
* `.min()`, `.max()`, `.trim()`, `.toLowerCase()`, `.optional()`, `.nullish()`, etc.
*
* @example
* ```ts
* zEmail()
* zEmail().min(1).max(254)
* zEmail().trim().toLowerCase()
* zEmail('Email is invalid')
* zEmail({ message: 'Email is invalid' })
* ```
*/
export const zEmail = (options?: string | { message?: string }) => {
const message =
typeof options === 'string' ? options : (options?.message ?? DEFAULT_EMAIL_MESSAGE);
return z.string().regex(EMAIL_REGEX, { message });
};