fix: add tests

This commit is contained in:
David Nguyen
2026-02-23 15:46:38 +11:00
parent e2b3597c36
commit e2794ec42b
20 changed files with 2646 additions and 428 deletions
@@ -1,9 +1,17 @@
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
import { useSearchParams } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import {
DEFAULT_EDITOR_CONFIG,
type EnvelopeEditorConfig,
type TEditorEnvelope,
} from '@documenso/lib/types/envelope-editor';
import { trpc } from '@documenso/trpc/react';
import type { TSetEnvelopeFieldsResponse } from '@documenso/trpc/server/envelope-router/set-envelope-fields.types';
import type { TSetEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/set-envelope-recipients.types';
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
@@ -11,7 +19,6 @@ import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
import { useToast } from '@documenso/ui/primitives/use-toast';
import type { TDocumentEmailSettings } from '../../types/document-email';
import type { TEnvelope } from '../../types/envelope';
import { formatDocumentsPath, formatTemplatesPath } from '../../utils/teams';
import { useEditorFields } from '../hooks/use-editor-fields';
import type { TLocalField } from '../hooks/use-editor-fields';
@@ -38,14 +45,20 @@ export const useDebounceFunction = <Args extends unknown[]>(
);
};
export type EnvelopeEditorStep = 'upload' | 'addFields' | 'preview';
type UpdateEnvelopePayload = Pick<TUpdateEnvelopeRequest, 'data' | 'meta'>;
type EnvelopeEditorProviderValue = {
envelope: TEnvelope;
editorConfig: EnvelopeEditorConfig;
envelope: TEditorEnvelope;
isEmbedded: boolean;
isDocument: boolean;
isTemplate: boolean;
setLocalEnvelope: (localEnvelope: Partial<TEnvelope>) => void;
setLocalEnvelope: (localEnvelope: Partial<TEditorEnvelope>) => void;
updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void;
updateEnvelopeAsync: (envelopeUpdates: UpdateEnvelopePayload) => Promise<void>;
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
@@ -57,7 +70,7 @@ type EnvelopeEditorProviderValue = {
editorRecipients: ReturnType<typeof useEditorRecipients>;
isAutosaving: boolean;
flushAutosave: () => Promise<void>;
flushAutosave: () => Promise<TEditorEnvelope>;
autosaveError: boolean;
relativePath: {
@@ -68,12 +81,14 @@ type EnvelopeEditorProviderValue = {
templateRootPath: string;
};
navigateToStep: (step: EnvelopeEditorStep) => Promise<void>;
syncEnvelope: () => Promise<void>;
};
interface EnvelopeEditorProviderProps {
children: React.ReactNode;
initialEnvelope: TEnvelope;
editorConfig?: EnvelopeEditorConfig;
initialEnvelope: TEditorEnvelope;
}
const EnvelopeEditorContext = createContext<EnvelopeEditorProviderValue | null>(null);
@@ -90,14 +105,29 @@ export const useCurrentEnvelopeEditor = () => {
export const EnvelopeEditorProvider = ({
children,
editorConfig = DEFAULT_EDITOR_CONFIG,
initialEnvelope,
}: EnvelopeEditorProviderProps) => {
const { t } = useLingui();
const { toast } = useToast();
const [envelope, setEnvelope] = useState(initialEnvelope);
const [_searchParams, setSearchParams] = useSearchParams();
const [envelope, _setEnvelope] = useState(initialEnvelope);
const [autosaveError, setAutosaveError] = useState<boolean>(false);
const envelopeRef = useRef(initialEnvelope);
const setEnvelope: typeof _setEnvelope = (action) => {
_setEnvelope((prev) => {
const next = typeof action === 'function' ? action(prev) : action;
envelopeRef.current = next;
return next;
});
};
const isEmbedded = editorConfig.embeded !== undefined;
const editorFields = useEditorFields({
envelope,
handleFieldsUpdate: (fields) => setFieldsDebounced(fields),
@@ -107,61 +137,35 @@ export const EnvelopeEditorProvider = ({
envelope,
});
const envelopeUpdateMutationQuery = trpc.envelope.update.useMutation({
onSuccess: (response, input) => {
setEnvelope({
...envelope,
...response,
documentMeta: {
...envelope.documentMeta,
...input.meta,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
emailSettings: (input.meta?.emailSettings ||
null) as unknown as TDocumentEmailSettings | null,
},
});
const setRecipientsMutation = trpc.envelope.recipient.set.useMutation();
const setFieldsMutation = trpc.envelope.field.set.useMutation();
const updateEnvelopeMutation = trpc.envelope.update.useMutation();
setAutosaveError(false);
},
onError: (err) => {
console.error(err);
/**
* Handles debouncing the recipients updates to the server.
*
* Will set the local envelope recipients and fields after the update is complete.
*/
const {
triggerSave: setRecipientsDebounced,
flush: flushSetRecipients,
isPending: isRecipientsMutationPending,
} = useEnvelopeAutosave(async (localRecipients: TSetEnvelopeRecipientsRequest['recipients']) => {
try {
let recipients: TEditorEnvelope['recipients'] = [];
setAutosaveError(true);
if (!isEmbedded) {
const response = await setRecipientsMutation.mutateAsync({
envelopeId: envelope.id,
envelopeType: envelope.type,
recipients: localRecipients,
});
toast({
title: t`Save failed`,
description: t`We encountered an error while attempting to save your changes. Your changes cannot be saved at this time.`,
variant: 'destructive',
duration: 7500,
});
},
});
recipients = response.data;
} else {
recipients = mapLocalRecipientsToRecipients({ envelope, localRecipients });
}
const envelopeFieldSetMutationQuery = trpc.envelope.field.set.useMutation({
onSuccess: ({ data: fields }) => {
setEnvelope((prev) => ({
...prev,
fields,
}));
setAutosaveError(false);
},
onError: (err) => {
console.error(err);
setAutosaveError(true);
toast({
title: t`Save failed`,
description: t`We encountered an error while attempting to save your changes. Your changes cannot be saved at this time.`,
variant: 'destructive',
duration: 7500,
});
},
});
const envelopeRecipientSetMutationQuery = trpc.envelope.recipient.set.useMutation({
onSuccess: ({ data: recipients }) => {
setEnvelope((prev) => ({
...prev,
recipients,
@@ -178,8 +182,7 @@ export const EnvelopeEditorProvider = ({
);
setAutosaveError(false);
},
onError: (err) => {
} catch (err) {
console.error(err);
setAutosaveError(true);
@@ -190,58 +193,137 @@ export const EnvelopeEditorProvider = ({
variant: 'destructive',
duration: 7500,
});
},
});
const {
triggerSave: setRecipientsDebounced,
flush: setRecipientsAsync,
isPending: isRecipientsMutationPending,
} = useEnvelopeAutosave(async (recipients: TSetEnvelopeRecipientsRequest['recipients']) => {
await envelopeRecipientSetMutationQuery.mutateAsync({
envelopeId: envelope.id,
envelopeType: envelope.type,
recipients,
});
}
}, 1000);
const setRecipientsAsync = async (
localRecipients: TSetEnvelopeRecipientsRequest['recipients'],
) => {
setRecipientsDebounced(localRecipients);
await flushSetRecipients();
};
/**
* Handles debouncing the fields updates to the server.
*
* Will set the local envelope fields after the update is complete.
*/
const {
triggerSave: setFieldsDebounced,
flush: setFieldsAsync,
flush: flushSetFields,
isPending: isFieldsMutationPending,
} = useEnvelopeAutosave(async (localFields: TLocalField[]) => {
const envelopeFields = await envelopeFieldSetMutationQuery.mutateAsync({
envelopeId: envelope.id,
envelopeType: envelope.type,
fields: localFields,
});
try {
let fields: TSetEnvelopeFieldsResponse['data'] = [];
// Insert the IDs into the local fields.
envelopeFields.data.forEach((field) => {
const localField = localFields.find((localField) => localField.formId === field.formId);
if (!isEmbedded) {
const response = await setFieldsMutation.mutateAsync({
envelopeId: envelope.id,
envelopeType: envelope.type,
fields: localFields,
});
if (localField && !localField.id) {
localField.id = field.id;
editorFields.setFieldId(localField.formId, field.id);
fields = response.data;
} else {
fields = mapLocalFieldsToFields({ envelope, localFields });
}
});
setEnvelope((prev) => ({
...prev,
fields,
}));
setAutosaveError(false);
// Insert the IDs into the local fields.
fields.forEach((field) => {
const localField = localFields.find((localField) => localField.formId === field.formId);
if (localField && !localField.id) {
localField.id = field.id;
editorFields.setFieldId(localField.formId, field.id);
}
});
} catch (err) {
console.error(err);
setAutosaveError(true);
toast({
title: t`Save failed`,
description: t`We encountered an error while attempting to save your changes. Your changes cannot be saved at this time.`,
variant: 'destructive',
duration: 7500,
});
}
}, 2000);
const setFieldsAsync = async (localFields: TLocalField[]) => {
setFieldsDebounced(localFields);
await flushSetFields();
};
/**
* Handles debouncing the envelope updates to the server.
*
* Will set the local envelope after the update is complete.
*/
const {
triggerSave: setEnvelopeDebounced,
flush: setEnvelopeAsync,
triggerSave: updateEnvelopeDebounced,
flush: flushUpdateEnvelope,
isPending: isEnvelopeMutationPending,
} = useEnvelopeAutosave(async (envelopeUpdates: UpdateEnvelopePayload) => {
await envelopeUpdateMutationQuery.mutateAsync({
envelopeId: envelope.id,
data: envelopeUpdates.data,
meta: envelopeUpdates.meta,
});
} = useEnvelopeAutosave(async ({ data, meta }: UpdateEnvelopePayload) => {
try {
const response = !isEmbedded
? await updateEnvelopeMutation.mutateAsync({
envelopeId: envelope.id,
data,
meta,
})
: {};
setEnvelope((prev) => ({
...prev,
...data,
authOptions: {
globalAccessAuth: data?.globalAccessAuth || [],
globalActionAuth: data?.globalActionAuth || [],
},
...response,
documentMeta: {
...prev.documentMeta,
...meta,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
emailSettings: (meta?.emailSettings || null) as unknown as TDocumentEmailSettings | null,
},
}));
setAutosaveError(false);
} catch (err) {
console.error(err);
setAutosaveError(true);
toast({
title: t`Save failed`,
description: t`We encountered an error while attempting to save your changes. Your changes cannot be saved at this time.`,
variant: 'destructive',
duration: 7500,
});
}
}, 1000);
const updateEnvelopeAsync = async (envelopeUpdates: UpdateEnvelopePayload) => {
updateEnvelopeDebounced(envelopeUpdates);
await flushUpdateEnvelope();
};
/**
* Updates the local envelope and debounces the update to the server.
*
* Use this when you want to update the local envelope immediately while debouncing
* the actual update to the server.
*/
const updateEnvelope = (envelopeUpdates: UpdateEnvelopePayload) => {
setEnvelope((prev) => ({
@@ -253,14 +335,7 @@ export const EnvelopeEditorProvider = ({
},
}));
setEnvelopeDebounced(envelopeUpdates);
};
const updateEnvelopeAsync = async (envelopeUpdates: UpdateEnvelopePayload) => {
await envelopeUpdateMutationQuery.mutateAsync({
envelopeId: envelope.id,
...envelopeUpdates,
});
updateEnvelopeDebounced(envelopeUpdates);
};
const getRecipientColorKey = useCallback(
@@ -276,12 +351,13 @@ export const EnvelopeEditorProvider = ({
[envelope.recipients],
);
const { refetch: reloadEnvelope, isLoading: isReloadingEnvelope } = trpc.envelope.get.useQuery(
const { refetch: reloadEnvelope } = trpc.envelope.get.useQuery(
{
envelopeId: envelope.id,
},
{
initialData: envelope,
enabled: !isEmbedded,
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
},
);
@@ -293,6 +369,11 @@ export const EnvelopeEditorProvider = ({
const syncEnvelope = async () => {
await flushAutosave();
// Bypass syncing for embedded mode.
if (isEmbedded) {
return;
}
const fetchedEnvelopeData = await reloadEnvelope();
if (fetchedEnvelopeData.data) {
@@ -302,55 +383,89 @@ export const EnvelopeEditorProvider = ({
recipients: fetchedEnvelopeData.data.recipients,
documentMeta: fetchedEnvelopeData.data.documentMeta,
});
editorFields.resetForm(fetchedEnvelopeData.data.fields);
}
};
const setLocalEnvelope = (localEnvelope: Partial<TEnvelope>) => {
const setLocalEnvelope = (localEnvelope: Partial<TEditorEnvelope>) => {
setEnvelope((prev) => ({ ...prev, ...localEnvelope }));
};
const isAutosaving = useMemo(() => {
return (
envelopeFieldSetMutationQuery.isPending ||
envelopeRecipientSetMutationQuery.isPending ||
envelopeUpdateMutationQuery.isPending ||
isFieldsMutationPending ||
isRecipientsMutationPending ||
isEnvelopeMutationPending
);
}, [
envelopeFieldSetMutationQuery.isPending,
envelopeRecipientSetMutationQuery.isPending,
envelopeUpdateMutationQuery.isPending,
isFieldsMutationPending,
isRecipientsMutationPending,
isEnvelopeMutationPending,
]);
return isFieldsMutationPending || isRecipientsMutationPending || isEnvelopeMutationPending;
}, [isFieldsMutationPending, isRecipientsMutationPending, isEnvelopeMutationPending]);
const relativePath = useMemo(() => {
const documentRootPath = formatDocumentsPath(envelope.team.url);
const templateRootPath = formatTemplatesPath(envelope.team.url);
let documentRootPath = formatDocumentsPath(envelope.team.url);
let templateRootPath = formatTemplatesPath(envelope.team.url);
const basePath = envelope.type === EnvelopeType.DOCUMENT ? documentRootPath : templateRootPath;
let envelopePath = `${basePath}/${envelope.id}`;
let editorPath = `${basePath}/${envelope.id}/edit`;
if (editorConfig.embeded) {
let embeddedEditorPath =
editorConfig.embeded.mode === 'edit'
? `/embed/v2/authoring/envelope/edit/${envelope.id}`
: `/embed/v2/authoring/envelope/create`;
embeddedEditorPath += `?token=${editorConfig.embeded.presignToken}`;
// Todo: Embeds - This should be thought about more.
envelopePath = embeddedEditorPath;
editorPath = embeddedEditorPath;
documentRootPath = embeddedEditorPath;
templateRootPath = embeddedEditorPath;
}
return {
basePath,
envelopePath: `${basePath}/${envelope.id}`,
editorPath: `${basePath}/${envelope.id}/edit`,
envelopePath,
editorPath,
documentRootPath,
templateRootPath,
};
}, [envelope.type, envelope.id]);
const flushAutosave = async (): Promise<void> => {
await Promise.all([setFieldsAsync(), setRecipientsAsync(), setEnvelopeAsync()]);
const navigateToStep = async (step: EnvelopeEditorStep) => {
setSearchParams((prev) => {
const newParams = new URLSearchParams(prev);
if (step === 'upload') {
newParams.delete('step');
} else {
newParams.set('step', step);
}
return newParams;
});
await flushAutosave();
resetForms();
};
const resetForms = () => {
editorRecipients.resetForm({
recipients: envelopeRef.current.recipients,
documentMeta: envelopeRef.current.documentMeta,
});
editorFields.resetForm(envelopeRef.current.fields);
};
const flushAutosave = async (): Promise<TEditorEnvelope> => {
await Promise.all([flushSetFields(), flushSetRecipients(), flushUpdateEnvelope()]);
return envelopeRef.current;
};
return (
<EnvelopeEditorContext.Provider
value={{
editorConfig,
envelope,
isEmbedded,
isDocument: envelope.type === EnvelopeType.DOCUMENT,
isTemplate: envelope.type === EnvelopeType.TEMPLATE,
setLocalEnvelope,
@@ -366,9 +481,107 @@ export const EnvelopeEditorProvider = ({
isAutosaving,
relativePath,
syncEnvelope,
navigateToStep,
}}
>
{children}
</EnvelopeEditorContext.Provider>
);
};
type MapLocalRecipientsToRecipientsOptions = {
envelope: TEditorEnvelope;
localRecipients: TSetEnvelopeRecipientsRequest['recipients'];
};
const mapLocalRecipientsToRecipients = ({
envelope,
localRecipients,
}: MapLocalRecipientsToRecipientsOptions): TEditorEnvelope['recipients'] => {
let smallestRecipientId = localRecipients.reduce((min, recipient) => {
if (recipient.id && recipient.id < min) {
return recipient.id;
}
return min;
}, -1);
return localRecipients.map((recipient) => {
const foundRecipient = envelope.recipients.find((recipient) => recipient.id === recipient.id);
let recipientId = recipient.id;
if (recipientId === undefined) {
recipientId = smallestRecipientId;
smallestRecipientId--;
}
return {
id: recipientId,
envelopeId: envelope.id,
email: recipient.email,
name: recipient.name,
token: foundRecipient?.token || '',
documentDeletedAt: foundRecipient?.documentDeletedAt || null,
expired: foundRecipient?.expired || null,
signedAt: foundRecipient?.signedAt || null,
authOptions:
recipient.actionAuth.length > 0
? { actionAuth: recipient.actionAuth, accessAuth: [] }
: null,
signingOrder: recipient.signingOrder ?? null,
rejectionReason: foundRecipient?.rejectionReason || null,
role: recipient.role,
readStatus: foundRecipient?.readStatus || ReadStatus.NOT_OPENED,
signingStatus: foundRecipient?.signingStatus || SigningStatus.NOT_SIGNED,
sendStatus: foundRecipient?.sendStatus || SendStatus.NOT_SENT,
};
});
};
type MapLocalFieldsToFieldsOptions = {
localFields: TLocalField[];
envelope: TEditorEnvelope;
};
const mapLocalFieldsToFields = ({
envelope,
localFields,
}: MapLocalFieldsToFieldsOptions): TSetEnvelopeFieldsResponse['data'] => {
let smallestFieldId = localFields.reduce((min, field) => {
if (field.id && field.id < min) {
return field.id;
}
return min;
}, -1);
return localFields.map((field) => {
const foundField = envelope.fields.find((envelopeField) => envelopeField.id === field.id);
let fieldId = field.id;
if (fieldId === undefined) {
fieldId = smallestFieldId;
smallestFieldId--;
}
return {
...field,
formId: field.formId,
id: fieldId,
envelopeId: envelope.id,
envelopeItemId: field.envelopeItemId,
type: field.type,
recipientId: field.recipientId,
positionX: new Prisma.Decimal(field.positionX),
positionY: new Prisma.Decimal(field.positionY),
width: new Prisma.Decimal(field.width),
height: new Prisma.Decimal(field.height),
secondaryId: foundField?.secondaryId || '',
inserted: foundField?.inserted || false,
customText: foundField?.customText || '',
fieldMeta: field.fieldMeta || null,
};
});
};
@@ -11,6 +11,8 @@ import React from 'react';
import type { Field, Recipient } from '@prisma/client';
import { PDF_IMAGE_RENDER_SCALE } from '@documenso/lib/constants/pdf-viewer';
import { PRESIGNED_ENVELOPE_ITEM_ID_PREFIX } from '@documenso/lib/utils/embed-config';
import type { TGetEnvelopeItemsMetaResponse } from '@documenso/remix/server/api/files/files.types';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
@@ -48,7 +50,13 @@ type EnvelopeRenderOverrideSettings = {
showRecipientSigningStatus?: boolean;
};
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
type EnvelopeRenderItem = {
id: string;
title: string;
order: number;
envelopeId: string;
data?: Uint8Array | null;
};
type EnvelopeRenderProviderValue = {
version: DocumentDataVersion;
@@ -76,7 +84,12 @@ interface EnvelopeRenderProviderProps {
*/
version: DocumentDataVersion;
envelope: Pick<TEnvelope, 'id' | 'envelopeItems' | 'status' | 'type'>;
envelope: Pick<TEnvelope, 'id' | 'status' | 'type'>;
/**
* The envelope items to render.
*/
envelopeItems: EnvelopeRenderItem[];
/**
* Optional fields which are passed down to renderers for custom rendering needs.
@@ -100,6 +113,13 @@ interface EnvelopeRenderProviderProps {
*/
token: string | undefined;
/**
* The presign token to access the envelope.
*
* If not provided, it will be assumed that the current user can access the document.
*/
presignToken?: string | undefined;
/**
* Custom override settings for generic page renderers.
*/
@@ -124,8 +144,10 @@ export const useCurrentEnvelopeRender = () => {
export const EnvelopeRenderProvider = ({
children,
envelope,
envelopeItems: envelopeItemsFromProps,
fields,
token,
presignToken,
recipients = [],
version,
overrideSettings,
@@ -144,12 +166,12 @@ export const EnvelopeRenderProvider = ({
const fetchStartedAtRef = useRef<number>(0);
const envelopeItems = useMemo(
() => envelope.envelopeItems.sort((a, b) => a.order - b.order),
[envelope.envelopeItems],
() => envelopeItemsFromProps.sort((a, b) => a.order - b.order),
[envelopeItemsFromProps],
);
const [currentItem, setCurrentItem] = useState<EnvelopeRenderItem | null>(
envelope.envelopeItems[0] ?? null,
envelopeItems[0] ?? null,
);
/**
@@ -157,10 +179,23 @@ export const EnvelopeRenderProvider = ({
*/
useEffect(() => {
void fetchEnvelopeRenderData();
}, [envelope.id, envelope.envelopeItems.length, token, version]);
}, [envelope.id, envelopeItems.length, token, version]);
const fetchEnvelopeRenderData = async () => {
if (!envelope.id || envelope.envelopeItems.length === 0) {
if (envelopeItems.length === 0) {
return;
}
// Render certain envelope items locally, such as embedded.
// No envelope ID means it's in embedded create mode.
if (
!envelope.id ||
envelopeItems.some(
(item) => item.id.startsWith(PRESIGNED_ENVELOPE_ITEM_ID_PREFIX) && item.data,
)
) {
await handleLocalFileFetch();
return;
}
@@ -175,6 +210,7 @@ export const EnvelopeRenderProvider = ({
const metaUrl = getEnvelopeItemMetaUrl({
envelopeId: envelope.id,
token,
presignToken,
});
const response = await fetch(metaUrl);
@@ -201,6 +237,7 @@ export const EnvelopeRenderProvider = ({
documentDataId: item.documentDataId,
pageIndex,
token,
presignToken,
version,
});
@@ -228,8 +265,49 @@ export const EnvelopeRenderProvider = ({
}
};
const handleLocalFileFetch = async () => {
setEnvelopeItemsMetaLoadingState('loading');
try {
// Build a map of envelope items by ID
const metaMap: Record<string, BasePageRenderData[]> = {};
// Dynamically import "pdfToImagesClientSide" function to prevent bundling.
const { pdfToImagesClientSide } = await import(
'@documenso/lib/server-only/ai/pdf-to-images.client'
);
for (const item of envelopeItems) {
if (item.data) {
// Clone the buffer so PDF.js can transfer it to its worker without detaching the one in state
const pdfBytes = new Uint8Array(structuredClone(item.data));
const pdfImages = await pdfToImagesClientSide(pdfBytes, {
scale: PDF_IMAGE_RENDER_SCALE,
});
metaMap[item.id] = pdfImages.map((image) => ({
envelopeItemId: item.id,
documentDataId: item.id,
pageIndex: image.pageIndex,
pageNumber: image.pageIndex + 1,
pageWidth: image.width,
pageHeight: image.height,
imageUrl: image.image,
}));
}
}
setEnvelopeItemsMeta(metaMap);
setEnvelopeItemsMetaLoadingState('loaded');
} catch (error) {
console.error('Failed to load envelope data:', error);
setEnvelopeItemsMetaLoadingState('error');
}
};
const setCurrentEnvelopeItem = (envelopeItemId: string) => {
const foundItem = envelope.envelopeItems.find((item) => item.id === envelopeItemId);
const foundItem = envelopeItems.find((item) => item.id === envelopeItemId);
setCurrentItem(foundItem ?? null);
};
+11 -2
View File
@@ -147,6 +147,14 @@ export const DEFAULT_EDITOR_CONFIG: EnvelopeEditorConfig = {
},
};
/**
* The default configuration for the embedded editor. This is merged with whatever is provided
* by the embedded hash.
*
* This is duplicated in the embedded repo playground
*
* /playground/src/components/embedddings/envelope-feature.ts
*/
export const DEFAULT_EMBEDDED_EDITOR_CONFIG = {
general: {
allowConfigureEnvelopeTitle: true,
@@ -179,7 +187,7 @@ export const DEFAULT_EMBEDDED_EDITOR_CONFIG = {
allowDelete: true,
},
recipients: {
allowAIDetection: true,
allowAIDetection: false,
allowConfigureSigningOrder: true,
allowConfigureDictateNextSigner: true,
allowApproverRole: true,
@@ -188,7 +196,7 @@ export const DEFAULT_EMBEDDED_EDITOR_CONFIG = {
allowAssistantRole: true,
},
fields: {
allowAIDetection: true,
allowAIDetection: false,
},
} as const satisfies EnvelopeEditorConfig;
@@ -258,6 +266,7 @@ export const ZEditorEnvelopeSchema = EnvelopeSchema.pick({
order: true,
})
.extend({
// Only used for embedded.
data: z.instanceof(Uint8Array).optional(),
})
.array(),
+1 -7
View File
@@ -1,4 +1,3 @@
import { msg } from '@lingui/core/macro';
import { z } from 'zod';
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
@@ -114,10 +113,5 @@ export const ZEnvelopeRecipientManySchema = ZRecipientManySchema.omit({
export const ZRecipientEmailSchema = z.union([
z.literal(''),
z
.string()
.trim()
.toLowerCase()
.email({ message: msg`Invalid email`.id })
.max(254),
z.string().trim().toLowerCase().email({ message: 'Invalid email' }).max(254),
]);