fix: reviewed

This commit is contained in:
David Nguyen
2026-08-26 16:01:30 +10:00
parent 63c01fb814
commit b20a726571
23 changed files with 543 additions and 130 deletions
@@ -6,7 +6,7 @@ import { DEFAULT_EMBEDDED_EDITOR_CONFIG } from '@documenso/lib/types/envelope-ed
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { Page } from '@playwright/test';
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { apiSignin } from './authentication';
@@ -264,8 +264,6 @@ export const getRecipientRows = (root: Page) =>
export const getRecipientRemoveButtons = (root: Page) => root.locator('[data-testid="remove-signer-button"]');
export const getSigningOrderInputs = (root: Page) => root.locator('[data-testid="signing-order-input"]');
export const clickEnvelopeEditorStep = async (root: Page, stepId: 'upload' | 'addFields' | 'preview') => {
await root.waitForTimeout(200);
await root.locator(`[data-testid="envelope-editor-step-${stepId}"]`).first().click();
@@ -335,10 +333,334 @@ export const toggleAllowDictateSigners = async (root: Page, enabled: boolean) =>
}
};
export const setSigningOrderValue = async (root: Page, index: number, value: number) => {
const input = getSigningOrderInputs(root).nth(index);
await input.fill(value.toString());
await input.blur();
/**
* Performs a mouse-based drag from a drag handle onto a target element.
*
* `@hello-pangea/dnd` only starts a drag once the pointer travels a small
* distance while pressed, and it hit-tests drop targets using the CENTRE of
* the dragged element — not the cursor. Since drag handles sit at the edge of
* wide rows/cards, the cursor destination is compensated so the dragged
* element's centre lands on the target's centre.
*/
export const dragHandleToTarget = async (
root: Page,
handle: Locator,
target: Locator,
options: { activeClass: string },
) => {
const { activeClass } = options;
await handle.scrollIntoViewIfNeeded();
const handleBox = await handle.boundingBox();
if (!handleBox) {
throw new Error('Unable to resolve drag handle position');
}
const startX = handleBox.x + handleBox.width / 2;
const startY = handleBox.y + handleBox.height / 2;
await root.mouse.move(startX, startY);
await root.mouse.down();
// Exceed the drag activation threshold, then wait for drag-dependent layout
// (e.g. expanding gap drop-zones) to settle before resolving positions.
const cursorX = startX + 8;
const cursorY = startY;
await root.mouse.move(cursorX, cursorY, { steps: 2 });
await root.waitForTimeout(300);
// The dragged element is the handle's draggable ancestor; while dragging it
// is fixed-positioned and follows the cursor at a constant offset. Drop
// targeting uses the dragged element's CENTRE, not the cursor, so the
// cursor destination is compensated by that offset.
const draggedElement = handle.locator('xpath=ancestor-or-self::*[@data-rfd-draggable-id][1]');
const draggedBox = await draggedElement.boundingBox();
const targetBox = await target.boundingBox();
if (!draggedBox || !targetBox) {
await root.mouse.up();
throw new Error('Unable to resolve drag positions');
}
const itemOffsetX = draggedBox.x + draggedBox.width / 2 - cursorX;
const itemOffsetY = draggedBox.y + draggedBox.height / 2 - cursorY;
const hasBecomeActive = async () => {
const className = await target.getAttribute('class');
return Boolean(className?.includes(activeClass));
};
// The highlight class is rendered from the library's own drag state, so it
// cannot disagree with where a drop will land — both phases below only drop
// once the target reports the drag as over it AND that state survives a
// short confirmation dwell (it can flicker while crossing a card's
// reorder/combine boundary).
//
// The cursor is always clamped inside the viewport: moving outside the
// window cancels the drag (pointercancel), and holding near the bottom edge
// lets the library auto-scroll the target up to the cursor instead.
const viewportHeight = root.viewportSize()?.height ?? 720;
const maxCursorY = viewportHeight - 40;
const confirmAndDrop = async () => {
if (!(await hasBecomeActive())) {
return false;
}
await root.waitForTimeout(150);
if (!(await hasBecomeActive())) {
return false;
}
await root.mouse.up();
return true;
};
let hasDropped = false;
// Crawl-and-drop: approach from above and inch downward through the
// corridor. Captured drop-target geometry can drift a few pixels from the
// live layout for small targets, so a slow traversal is the reliable way to
// hit them.
const crawlX = targetBox.x + targetBox.width / 2 - itemOffsetX;
const crawlStartY = Math.min(targetBox.y + targetBox.height / 2 - itemOffsetY - 140, maxCursorY);
await root.mouse.move(crawlX, crawlStartY, { steps: 15 });
await root.waitForTimeout(150);
for (let step = 1; step <= 80; step += 1) {
if (await confirmAndDrop()) {
hasDropped = true;
break;
}
await root.mouse.move(crawlX, Math.min(crawlStartY + step * 6, maxCursorY), { steps: 2 });
await root.waitForTimeout(70);
}
if (!hasDropped) {
await root.mouse.up();
}
await root.waitForTimeout(400);
};
export const getRecipientStepCards = (root: Page) => root.locator('[data-testid="recipient-step-card"]');
export const getRecipientStepGaps = (root: Page) => root.locator('[data-testid="recipient-step-gap"]');
export const getStepDragHandles = (root: Page) => root.locator('[data-testid="step-drag-handle"]');
export const getRecipientRowDragHandles = (root: Page) => root.locator('[data-testid="recipient-row-drag-handle"]');
/**
* Drags a whole group card onto another card, merging the two groups.
*
* Uses @hello-pangea/dnd's keyboard drag mode: mouse-emulated combines are
* unreliable because approaching a card traverses its reorder edge, which
* displaces the target away from the cursor. Keyboard drags step through
* positions (including combine states) deterministically.
*/
export const dragGroupCardOntoCard = async (root: Page, sourceCardIndex: number, targetCardIndex: number) => {
const handle = getStepDragHandles(root).nth(sourceCardIndex);
const target = getRecipientStepCards(root).nth(targetCardIndex);
await handle.scrollIntoViewIfNeeded();
await handle.focus();
// Lift.
await root.keyboard.press('Space');
await root.waitForTimeout(250);
const direction = targetCardIndex < sourceCardIndex ? 'ArrowUp' : 'ArrowDown';
for (let press = 0; press < 4; press += 1) {
await root.keyboard.press(direction);
await root.waitForTimeout(250);
const targetClassName = await target.getAttribute('class');
if (targetClassName?.includes('ring-primary')) {
// Drop while the target reports the combine state.
await root.keyboard.press('Space');
await root.waitForTimeout(400);
return;
}
}
await root.keyboard.press('Escape');
throw new Error('Combine drag did not reach the target card');
};
/**
* Moves a group card one position up via keyboard drag. With combining
* enabled, the first ArrowUp enters the combine state with the card above and
* the second moves above it.
*/
export const moveGroupCardUp = async (root: Page, cardIndex: number) => {
const handle = getStepDragHandles(root).nth(cardIndex);
await handle.scrollIntoViewIfNeeded();
await handle.focus();
await root.keyboard.press('Space');
await root.waitForTimeout(250);
await root.keyboard.press('ArrowUp');
await root.waitForTimeout(250);
await root.keyboard.press('ArrowUp');
await root.waitForTimeout(250);
await root.keyboard.press('Space');
await root.waitForTimeout(400);
};
/**
* Drags a recipient row into a gap between group cards, extracting it into
* its own standalone group at that position.
*/
export const dragRecipientRowToGap = async (root: Page, rowIndex: number, gapIndex: number) => {
await dragHandleToTarget(
root,
getRecipientRowDragHandles(root).nth(rowIndex),
getRecipientStepGaps(root).nth(gapIndex),
// The marker class applied to a gap drop-zone while dragged over.
{ activeClass: 'gap-active' },
);
};
export type SweepRecipientRowOverCardResult = {
/**
* Whether any gap drop-zone activated during the sweep — proof the drag
* gesture itself was live, so "the card never activated" cannot be a
* false negative from a drag that silently failed to start.
*/
sawGapActive: boolean;
/**
* Whether the target card reported the row as a join target (`ring-primary`).
*/
sawCardActive: boolean;
/**
* Whether the row was dropped onto the card (only when it became active).
*/
dropped: boolean;
};
/**
* Drags a recipient row across a group card's body, dropping it to join the
* group as soon as the card activates. If the card never activates (e.g. the
* join drop-zone is disabled), the drag is cancelled with Escape so no
* accidental gap-drop mutates the order.
*
* Unlike `dragHandleToTarget`'s fixed-interval crawl, each sweep position
* polls for activation with a generous budget, which keeps the gesture
* reliable when rendering lags under parallel test load.
*/
export const sweepRecipientRowOverCard = async (
root: Page,
rowIndex: number,
cardIndex: number,
): Promise<SweepRecipientRowOverCardResult> => {
const handle = getRecipientRowDragHandles(root).nth(rowIndex);
const card = getRecipientStepCards(root).nth(cardIndex);
const result: SweepRecipientRowOverCardResult = {
sawGapActive: false,
sawCardActive: false,
dropped: false,
};
await handle.scrollIntoViewIfNeeded();
const handleBox = await handle.boundingBox();
if (!handleBox) {
throw new Error('Unable to resolve drag handle position');
}
const startX = handleBox.x + handleBox.width / 2;
const startY = handleBox.y + handleBox.height / 2;
await root.mouse.move(startX, startY);
await root.mouse.down();
// Exceed the drag activation threshold, then wait for drag-dependent
// layout (expanding drop-zones) to settle before resolving positions.
const cursorX = startX + 8;
const cursorY = startY;
await root.mouse.move(cursorX, cursorY, { steps: 2 });
await root.waitForTimeout(300);
// Drop targeting uses the dragged element's CENTRE, not the cursor, so
// cursor destinations are compensated by the constant cursor-to-centre
// offset captured at lift time.
const draggedElement = handle.locator('xpath=ancestor-or-self::*[@data-rfd-draggable-id][1]');
const draggedBox = await draggedElement.boundingBox();
const cardBox = await card.boundingBox();
if (!draggedBox || !cardBox) {
await root.mouse.up();
throw new Error('Unable to resolve drag positions');
}
const itemOffsetX = draggedBox.x + draggedBox.width / 2 - cursorX;
const itemOffsetY = draggedBox.y + draggedBox.height / 2 - cursorY;
const sweepX = cardBox.x + cardBox.width / 2 - itemOffsetX;
const sweepFromY = cardBox.y - itemOffsetY - 40;
const sweepToY = cardBox.y + cardBox.height - itemOffsetY + 80;
await root.mouse.move(sweepX, sweepFromY, { steps: 15 });
for (let y = sweepFromY; y <= sweepToY && !result.dropped; y += 8) {
await root.mouse.move(sweepX, y, { steps: 2 });
// Poll for activation: drag state is rendered on animation frames, so
// under load the classes can trail the cursor by hundreds of ms.
for (let tick = 0; tick < 6; tick += 1) {
const cardClassName = (await card.getAttribute('class')) ?? '';
if (cardClassName.includes('ring-primary')) {
result.sawCardActive = true;
await root.mouse.up();
result.dropped = true;
break;
}
if (!result.sawGapActive) {
const activeGapCount = await root.locator('[data-testid="recipient-step-gap"].gap-active').count();
result.sawGapActive = activeGapCount > 0;
}
await root.waitForTimeout(50);
}
}
if (!result.dropped) {
// Cancel rather than release: releasing over an active gap would extract
// the row into a new step, silently mutating the signing order.
await root.keyboard.press('Escape');
await root.waitForTimeout(100);
await root.mouse.up();
}
await root.waitForTimeout(400);
return result;
};
export const persistEmbeddedEnvelope = async (surface: TEnvelopeEditorSurface) => {
@@ -38,7 +38,9 @@ import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../uti
import { buildTeamWhereQuery } from '../../utils/teams';
import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-id';
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
import { assignDefaultRecipientSigningOrders } from '../signature-level/assign-default-recipient-signing-orders';
import { resolveSignatureLevel } from '../signature-level/resolve-signature-level';
import { getTeamSettings } from '../team/get-team-settings';
import { assertUserNotDisabledById } from '../user/assert-user-not-disabled';
@@ -280,6 +282,31 @@ export const createEnvelope = async ({
assertCompatibleRecipientRole({ signatureLevel, role: recipient.role });
}
const parsedDefaultRecipients =
settings.defaultRecipients && !bypassDefaultRecipients
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
: [];
const defaultRecipients: CreateEnvelopeRecipientOptions[] = parsedDefaultRecipients.map((recipient) => ({
email: recipient.email,
name: recipient.name,
role: recipient.role,
}));
// Assign default recipients signing orders if TSP is enabled since
// TSP envelopes require sequential signing.
const orderedDefaultRecipients = assignDefaultRecipientSigningOrders({
signatureLevel,
payloadRecipients: data.recipients ?? [],
defaultRecipients,
});
const recipientsToCreate = [...(data.recipients || []), ...orderedDefaultRecipients];
// The grouping assertion runs against the COMBINED set the envelope will
// actually hold, not just the payload.
assertCompatibleRecipientGrouping({ signatureLevel, recipients: recipientsToCreate });
const visibility = visibilityOverride || settings.documentVisibility;
const emailId = meta?.emailId;
@@ -403,21 +430,8 @@ export const createEnvelope = async ({
const firstEnvelopeItem = envelope.envelopeItems[0];
const defaultRecipients =
settings.defaultRecipients && !bypassDefaultRecipients
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
: [];
const mappedDefaultRecipients: CreateEnvelopeRecipientOptions[] = defaultRecipients.map((recipient) => ({
email: recipient.email,
name: recipient.name,
role: recipient.role,
}));
const allRecipients = [...(data.recipients || []), ...mappedDefaultRecipients];
await Promise.all(
allRecipients.map(async (recipient) => {
recipientsToCreate.map(async (recipient) => {
const recipientAuthOptions = createRecipientAuthOptions({
accessAuth: recipient.accessAuth ?? [],
actionAuth: recipient.actionAuth ?? [],
@@ -12,6 +12,7 @@ import type { EnvelopeIdOptions } from '../../utils/envelope';
import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
export interface CreateEnvelopeRecipientsOptions {
@@ -91,6 +92,13 @@ export const createEnvelopeRecipients = async ({
});
}
// Grouping is a property of the whole recipient set, so check the state the
// envelope will be left in rather than the incoming batch alone.
assertCompatibleRecipientGrouping({
signatureLevel: envelope.signatureLevel,
recipients: [...envelope.recipients, ...recipientsToCreate],
});
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
@@ -17,6 +17,7 @@ import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
export interface SetDocumentRecipientsOptions {
@@ -98,6 +99,11 @@ export const setDocumentRecipients = async ({
});
}
assertCompatibleRecipientGrouping({
signatureLevel: envelope.signatureLevel,
recipients,
});
const normalizedRecipients = recipients.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
@@ -12,6 +12,7 @@ import { nanoid } from '../../universal/id';
import { createRecipientAuthOptions } from '../../utils/document-auth';
import { type EnvelopeIdOptions, mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
export type SetTemplateRecipientsOptions = {
@@ -68,6 +69,11 @@ export const setTemplateRecipients = async ({ userId, teamId, id, recipients }:
});
}
assertCompatibleRecipientGrouping({
signatureLevel: envelope.signatureLevel,
recipients,
});
const normalizedRecipients = recipients.map((recipient) => {
// Force replace any changes to the name or email of the direct recipient.
if (envelope.directLink && recipient.id === envelope.directLink.directTemplateRecipientId) {
@@ -14,6 +14,7 @@ import { mapFieldToLegacyField } from '../../utils/fields';
import { canRecipientBeModified } from '../../utils/recipients';
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { assertCompatibleRecipientGrouping } from '../signature-level/assert-compatible-recipient-grouping';
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
export interface UpdateEnvelopeRecipientsOptions {
@@ -99,6 +100,16 @@ export const updateEnvelopeRecipients = async ({
});
}
assertCompatibleRecipientGrouping({
signatureLevel: envelope.signatureLevel,
// Combine the existing recipients with the new ones to see if the grouping is compatible.
recipients: envelope.recipients.map((existingRecipient) => {
const update = recipients.find((recipient) => recipient.id === existingRecipient.id);
return update ? { ...existingRecipient, ...update } : existingRecipient;
}),
});
const recipientsToUpdate = recipients.map((recipient) => {
const originalRecipient = envelope.recipients.find((existingRecipient) => existingRecipient.id === recipient.id);
@@ -0,0 +1,57 @@
import type { Recipient } from '@prisma/client';
import { isTspEnvelope } from '../../types/signature-level';
import { isCcRecipient } from '../../utils/recipients';
type AssignDefaultRecipientSigningOrdersOptions<T> = {
signatureLevel: string;
/**
* The recipients supplied by the caller, which should already be validated by
* {@link assertCompatibleRecipientGrouping}.
*/
payloadRecipients: Array<Pick<Recipient, 'role'> & { signingOrder?: number | null }>;
/**
* The team default recipients to append.
*/
defaultRecipients: T[];
};
/**
* Assigns distinct signing orders to default recipients appended to a
* TSP (AES/QES) envelope.
*
* Default recipients carry no signing order, so on a TSP envelope two or more of
* them would share the unordered tail step — a signing group, which TSP
* signatures cannot hold
*
* CC defaults are left unordered: they never sign and are ignored by the
* grouping assertion.
*
* SES envelopes pass through unchanged — shared steps are an SES feature.
*/
export const assignDefaultRecipientSigningOrders = <T extends Pick<Recipient, 'role'>>({
signatureLevel,
payloadRecipients,
defaultRecipients,
}: AssignDefaultRecipientSigningOrdersOptions<T>): Array<T & { signingOrder?: number }> => {
if (!isTspEnvelope({ signatureLevel })) {
return defaultRecipients;
}
let nextOrder =
payloadRecipients.reduce((highest, recipient) => Math.max(highest, recipient.signingOrder ?? 0), 0) + 1;
return defaultRecipients.map((recipient) => {
if (isCcRecipient(recipient)) {
return recipient;
}
const signingOrder = nextOrder;
nextOrder += 1;
return { ...recipient, signingOrder };
});
};
+11
View File
@@ -125,3 +125,14 @@ export type TEnvelopeRecipientLite = z.infer<typeof ZEnvelopeRecipientLiteSchema
export type TEnvelopeRecipientMany = z.infer<typeof ZEnvelopeRecipientManySchema>;
export const ZRecipientEmailSchema = z.union([z.literal(''), zEmail('Invalid email').trim().toLowerCase().max(254)]);
/**
* Signing order for a recipient, for use in request schemas.
*
* Response schemas intentionally do not use this: existing rows may hold values
* that predate the constraint, and reads must not fail because of it.
*/
export const ZRecipientSigningOrderSchema = z
.number()
.int('Signing order must be an integer')
.min(1, 'Signing order must be greater than 0');
+8 -8
View File
@@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest';
import {
extractRecipientToNewStep,
filterRecipientsInFirstSigningGroup,
flattenRecipientGroups,
getNextDictatableRecipient,
getRecipientsInActiveSigningStep,
groupRecipientsBySigningOrder,
isRecipientTurnBySigningOrder,
mergeSteps,
@@ -554,7 +554,7 @@ describe('isRecipientTurnBySigningOrder', () => {
});
});
describe('filterRecipientsInFirstSigningGroup', () => {
describe('getRecipientsInActiveSigningStep', () => {
const candidate = (
id: number,
signingOrder: number | null,
@@ -565,17 +565,17 @@ describe('filterRecipientsInFirstSigningGroup', () => {
it('returns every pending recipient sharing the lowest order', () => {
const recipients = [candidate(3, 2), candidate(4, 2), candidate(5, 3)];
expect(filterRecipientsInFirstSigningGroup(recipients).map((r) => r.id)).toEqual([3, 4]);
expect(getRecipientsInActiveSigningStep(recipients).map((r) => r.id)).toEqual([3, 4]);
});
it('returns an empty array for no pending recipients', () => {
expect(filterRecipientsInFirstSigningGroup([])).toEqual([]);
expect(getRecipientsInActiveSigningStep([])).toEqual([]);
});
it('excludes recipients that have already signed', () => {
const recipients = [candidate(1, 1, SigningStatus.SIGNED), candidate(2, 2)];
expect(filterRecipientsInFirstSigningGroup(recipients).map((r) => r.id)).toEqual([2]);
expect(getRecipientsInActiveSigningStep(recipients).map((r) => r.id)).toEqual([2]);
});
// A rejected recipient is not pending: advancing to them would re-activate
@@ -583,7 +583,7 @@ describe('filterRecipientsInFirstSigningGroup', () => {
it('excludes rejected recipients', () => {
const recipients = [candidate(1, 1, SigningStatus.REJECTED), candidate(2, 2)];
expect(filterRecipientsInFirstSigningGroup(recipients).map((r) => r.id)).toEqual([2]);
expect(getRecipientsInActiveSigningStep(recipients).map((r) => r.id)).toEqual([2]);
});
it('excludes CC recipients', () => {
@@ -592,13 +592,13 @@ describe('filterRecipientsInFirstSigningGroup', () => {
candidate(2, 2, SigningStatus.NOT_SIGNED),
];
expect(filterRecipientsInFirstSigningGroup(recipients).map((r) => r.id)).toEqual([2]);
expect(getRecipientsInActiveSigningStep(recipients).map((r) => r.id)).toEqual([2]);
});
it('returns an empty array when every recipient is signed or rejected', () => {
const recipients = [candidate(1, 1, SigningStatus.SIGNED), candidate(2, 2, SigningStatus.REJECTED)];
expect(filterRecipientsInFirstSigningGroup(recipients)).toEqual([]);
expect(getRecipientsInActiveSigningStep(recipients)).toEqual([]);
});
});
@@ -19,6 +19,7 @@ import {
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient';
import { zEmail } from '@documenso/lib/utils/zod';
import { RecipientRole } from '@documenso/prisma/client';
import { DocumentSigningOrder } from '@documenso/prisma/generated/types';
@@ -36,7 +37,7 @@ export const ZCreateEmbeddingDocumentRequestSchema = z.object({
email: zEmail(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
// We have an any cast so any changes here you need to update it in the embeding document edit page
// Search: "map<any>" to find it
fields: ZFieldAndMetaSchema.and(
@@ -19,7 +19,7 @@ import {
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
import { ZRecipientEmailSchema, ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
@@ -33,7 +33,7 @@ export const ZCreateEmbeddingTemplateRequestSchema = z.object({
email: ZRecipientEmailSchema,
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
// We have an any cast so any changes here you need to update it in the embeding document edit page
// Search: "map<any>" to find it
fields: ZFieldAndMetaSchema.and(
@@ -19,6 +19,7 @@ import {
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient';
import { zEmail } from '@documenso/lib/utils/zod';
import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/generated/types';
import { z } from 'zod';
@@ -35,7 +36,7 @@ export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
email: zEmail(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
// We have an any cast so any changes here you need to update it in the embeding document edit page
// Search: "map<any>" to find it
fields: ZFieldAndMetaSchema.and(
@@ -19,7 +19,7 @@ import {
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
import { ZRecipientEmailSchema, ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
@@ -35,7 +35,7 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
email: ZRecipientEmailSchema,
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
// We have an any cast so any changes here you need to update it in the embeding document edit page
// Search: "map<any>" to find it
fields: ZFieldAndMetaSchema.and(
@@ -1,5 +1,9 @@
import { ZRecipientAccessAuthTypesSchema, ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { ZEnvelopeRecipientLiteSchema, ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
import {
ZEnvelopeRecipientLiteSchema,
ZRecipientEmailSchema,
ZRecipientSigningOrderSchema,
} from '@documenso/lib/types/recipient';
import { RecipientRole } from '@prisma/client';
import { z } from 'zod';
@@ -19,7 +23,7 @@ export const ZCreateEnvelopeRecipientSchema = z.object({
email: ZRecipientEmailSchema,
name: z.string().max(255),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).default([]).optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).default([]).optional(),
});
@@ -1,14 +1,22 @@
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { ZRecipientEmailSchema, ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
import {
ZRecipientEmailSchema,
ZRecipientLiteSchema,
ZRecipientSigningOrderSchema,
} from '@documenso/lib/types/recipient';
import { EnvelopeType, RecipientRole } from '@prisma/client';
import { z } from 'zod';
export const ZSetEnvelopeRecipientSchema = z.object({
id: z.number().optional(),
clientId: z
.string()
.optional()
.describe('A temporary ID echoed back on the response so newly created recipients can be reconciled'),
email: ZRecipientEmailSchema,
name: z.string().max(255),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
});
@@ -22,7 +30,11 @@ export const ZSetEnvelopeRecipientsResponseSchema = z.object({
data: ZRecipientLiteSchema.omit({
documentId: true,
templateId: true,
}).array(),
})
.extend({
clientId: z.string().nullish(),
})
.array(),
});
export type TSetEnvelopeRecipientsRequest = z.infer<typeof ZSetEnvelopeRecipientsRequestSchema>;
@@ -5,7 +5,7 @@ import {
ZRecipientActionAuthSchema,
ZRecipientActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZRecipientLiteSchema, ZRecipientSchema } from '@documenso/lib/types/recipient';
import { ZRecipientLiteSchema, ZRecipientSchema, ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient';
import { zEmail } from '@documenso/lib/utils/zod';
import { RecipientRole } from '@prisma/client';
import { z } from 'zod';
@@ -27,7 +27,7 @@ export const ZCreateRecipientSchema = z.object({
email: zEmail().toLowerCase().min(1).max(254),
name: z.string().max(255),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).default([]).optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).default([]).optional(),
});
@@ -37,7 +37,7 @@ export const ZUpdateRecipientSchema = z.object({
email: zEmail().toLowerCase().min(1).max(254).optional(),
name: z.string().max(255).optional(),
role: z.nativeEnum(RecipientRole).optional(),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).default([]).optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).default([]).optional(),
});
@@ -86,7 +86,7 @@ export const ZSetDocumentRecipientsRequestSchema = z.object({
email: zEmail().toLowerCase().min(1).max(254),
name: z.string().max(255),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
}),
),
@@ -148,7 +148,7 @@ export const ZSetTemplateRecipientsRequestSchema = z.object({
),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
signingOrder: ZRecipientSigningOrderSchema.optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
}),
),