mirror of
https://github.com/documenso/documenso.git
synced 2026-10-04 02:33:54 +10:00
fix: reviewed
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import { getNextDictatableRecipient } from '@documenso/lib/utils/recipient-groups';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
@@ -223,27 +224,10 @@ export const DirectTemplateSigningForm = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sortedRecipients = template.recipients.sort((a, b) => {
|
||||
// Sort by signingOrder first (nulls last), then by id
|
||||
if (a.signingOrder === null && b.signingOrder === null) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
if (a.signingOrder === null) {
|
||||
return 1;
|
||||
}
|
||||
if (b.signingOrder === null) {
|
||||
return -1;
|
||||
}
|
||||
if (a.signingOrder === b.signingOrder) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
return a.signingOrder - b.signingOrder;
|
||||
return getNextDictatableRecipient({
|
||||
recipients: template.recipients,
|
||||
currentRecipientId: directRecipient.id,
|
||||
});
|
||||
|
||||
const currentIndex = sortedRecipients.findIndex((r) => r.id === directRecipient.id);
|
||||
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
|
||||
? sortedRecipients[currentIndex + 1]
|
||||
: undefined;
|
||||
}, [template.templateMeta?.signingOrder, template.recipients, directRecipient.id]);
|
||||
|
||||
return (
|
||||
@@ -435,7 +419,7 @@ export const DirectTemplateSigningForm = ({
|
||||
fields={localFields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
recipient={directRecipient}
|
||||
allowDictateNextSigner={nextRecipient && template.templateMeta?.allowDictateNextSigner}
|
||||
allowDictateNextSigner={Boolean(nextRecipient && template.templateMeta?.allowDictateNextSigner)}
|
||||
defaultNextSigner={nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+4
-2
@@ -100,8 +100,10 @@ export const DocumentSigningCompleteDialog = ({
|
||||
|
||||
const { isNameLocked, isEmailLocked } = useEmbedSigningContext() || {};
|
||||
|
||||
const canDictateNextSigner = allowDictateNextSigner && Boolean(defaultNextSigner);
|
||||
|
||||
const form = useForm<TNextSignerFormSchema>({
|
||||
resolver: allowDictateNextSigner ? zodResolver(ZNextSignerFormSchema) : undefined,
|
||||
resolver: canDictateNextSigner ? zodResolver(ZNextSignerFormSchema) : undefined,
|
||||
defaultValues: {
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
@@ -317,7 +319,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
{allowDictateNextSigner && defaultNextSigner && (
|
||||
{canDictateNextSigner && (
|
||||
<div className="mb-4 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<FormField
|
||||
|
||||
@@ -39,7 +39,11 @@ export type DocumentSigningFormProps = {
|
||||
}) => Promise<void>;
|
||||
isSubmitting: boolean;
|
||||
fieldsValidated: () => void;
|
||||
nextRecipient?: RecipientWithFields;
|
||||
/**
|
||||
* The dictatable next recipient, decided server-side. Only their identity
|
||||
* is needed — for the dictation flag and the prefilled inputs.
|
||||
*/
|
||||
nextRecipient?: Pick<Recipient, 'name' | 'email'>;
|
||||
};
|
||||
|
||||
export const DocumentSigningForm = ({
|
||||
@@ -84,6 +88,10 @@ export const DocumentSigningForm = ({
|
||||
return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id);
|
||||
}, [fieldsRequiringValidation, recipient]);
|
||||
|
||||
const allowDictateNextSigner = Boolean(nextRecipient && document.documentMeta?.allowDictateNextSigner);
|
||||
|
||||
const defaultNextSigner = nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined;
|
||||
|
||||
const localFieldsValidated = () => {
|
||||
setValidateUninsertedFields(true);
|
||||
fieldsValidated();
|
||||
@@ -151,10 +159,8 @@ export const DocumentSigningForm = ({
|
||||
completeDocument({ nextSigner, accessAuthOptions })
|
||||
}
|
||||
recipient={recipient}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
defaultNextSigner={
|
||||
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
|
||||
}
|
||||
allowDictateNextSigner={allowDictateNextSigner}
|
||||
defaultNextSigner={defaultNextSigner}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,8 +229,8 @@ export const DocumentSigningForm = ({
|
||||
onClose={() => !isAssistantSubmitting && setIsConfirmationDialogOpen(false)}
|
||||
onConfirm={handleAssistantConfirmDialogSubmit}
|
||||
isSubmitting={isAssistantSubmitting}
|
||||
allowDictateNextSigner={nextRecipient && document.documentMeta?.allowDictateNextSigner}
|
||||
defaultNextSigner={nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined}
|
||||
allowDictateNextSigner={allowDictateNextSigner}
|
||||
defaultNextSigner={defaultNextSigner}
|
||||
/>
|
||||
</form>
|
||||
) : (
|
||||
@@ -291,10 +297,8 @@ export const DocumentSigningForm = ({
|
||||
})
|
||||
}
|
||||
recipient={recipient}
|
||||
allowDictateNextSigner={nextRecipient && document.documentMeta?.allowDictateNextSigner}
|
||||
defaultNextSigner={
|
||||
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
|
||||
}
|
||||
allowDictateNextSigner={allowDictateNextSigner}
|
||||
defaultNextSigner={defaultNextSigner}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
+8
-29
@@ -23,7 +23,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Field } from '@prisma/client';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { FieldType, RecipientRole } from '@prisma/client';
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
@@ -61,6 +61,12 @@ export type DocumentSigningPageViewV1Props = {
|
||||
completedFields: CompletedField[];
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
/**
|
||||
* The dictatable next recipient, computed server-side over the FULL
|
||||
* recipient list — must not be re-derived from the role-scoped
|
||||
* `allRecipients`.
|
||||
*/
|
||||
nextRecipient?: Pick<Recipient, 'name' | 'email'>;
|
||||
branding: DocumentSigningBranding;
|
||||
includeSenderDetails: boolean;
|
||||
};
|
||||
@@ -72,6 +78,7 @@ export const DocumentSigningPageViewV1 = ({
|
||||
completedFields,
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
nextRecipient,
|
||||
includeSenderDetails,
|
||||
branding,
|
||||
}: DocumentSigningPageViewV1Props) => {
|
||||
@@ -142,34 +149,6 @@ export const DocumentSigningPageViewV1 = ({
|
||||
const selectedSigner = allRecipients?.find((r) => r.id === selectedSignerId);
|
||||
const targetSigner = recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null;
|
||||
|
||||
const nextRecipient = useMemo(() => {
|
||||
if (!documentMeta?.signingOrder || documentMeta.signingOrder !== 'SEQUENTIAL') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sortedRecipients = [...allRecipients].sort((a, b) => {
|
||||
// Sort by signingOrder first (nulls last), then by id
|
||||
if (a.signingOrder === null && b.signingOrder === null) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
if (a.signingOrder === null) {
|
||||
return 1;
|
||||
}
|
||||
if (b.signingOrder === null) {
|
||||
return -1;
|
||||
}
|
||||
if (a.signingOrder === b.signingOrder) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
return a.signingOrder - b.signingOrder;
|
||||
});
|
||||
|
||||
const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id);
|
||||
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
|
||||
? sortedRecipients[currentIndex + 1]
|
||||
: undefined;
|
||||
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
|
||||
|
||||
const pendingFields = fieldsRequiringValidation.filter((field) => !field.inserted);
|
||||
const hasPendingFields = pendingFields.length > 0;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/enve
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { isFieldUnsignedAndRequired, isRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing';
|
||||
import { getNextDictatableRecipient } from '@documenso/lib/utils/recipient-groups';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
|
||||
import { EnvelopeType, type Field, FieldType, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
@@ -290,32 +291,14 @@ export const EnvelopeSigningProvider = ({
|
||||
.filter((field) => field.inserted);
|
||||
|
||||
const nextRecipient = useMemo(() => {
|
||||
if (!envelope.documentMeta.signingOrder || envelope.documentMeta.signingOrder !== 'SEQUENTIAL') {
|
||||
if (envelope.documentMeta.signingOrder !== 'SEQUENTIAL') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortedRecipients = [...envelope.recipients].sort((a, b) => {
|
||||
// Sort by signingOrder first (nulls last), then by id
|
||||
if (a.signingOrder === null && b.signingOrder === null) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
if (a.signingOrder === null) {
|
||||
return 1;
|
||||
}
|
||||
if (b.signingOrder === null) {
|
||||
return -1;
|
||||
}
|
||||
if (a.signingOrder === b.signingOrder) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
return a.signingOrder - b.signingOrder;
|
||||
return getNextDictatableRecipient({
|
||||
recipients: envelope.recipients,
|
||||
currentRecipientId: recipient.id,
|
||||
});
|
||||
|
||||
const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id);
|
||||
|
||||
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
|
||||
? sortedRecipients[currentIndex + 1]
|
||||
: null;
|
||||
}, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]);
|
||||
|
||||
const signField = async (
|
||||
|
||||
@@ -43,6 +43,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
throw new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
if (document.internalVersion !== 1) {
|
||||
throw redirect(`${documentRootPath}/${document.envelopeId}/edit`);
|
||||
}
|
||||
|
||||
const documentVisibility = document.visibility;
|
||||
const currentTeamMemberRole = team.currentTeamRole;
|
||||
const isRecipient = document.recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
@@ -42,6 +42,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
throw redirect(templateRootPath);
|
||||
}
|
||||
|
||||
if (template.internalVersion !== 1) {
|
||||
throw redirect(`${templateRootPath}/${template.envelopeId}/edit`);
|
||||
}
|
||||
|
||||
return superLoaderJson({
|
||||
template: {
|
||||
...template,
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
+6
-2
@@ -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([]),
|
||||
}),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user