mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
d5ce222482
Adds Cloud Signature Consortium (CSC) integration for AES/QES signing against a configured TSP. v1 ships as instance-wide configuration via environment variables, with per-envelope signature level selection, license gating, and an OAuth-driven signing flow (capture + FIFO signers, SAD session, blocking/in-progress recipient pages). Includes signature level compatibility checks (role, signing order, dictate next signer), envelope mutability assertions, Prisma migration for signature level and CSC tables, and docs for the new signing certificate options.
623 lines
19 KiB
TypeScript
623 lines
19 KiB
TypeScript
import { IS_INSTANCE_CSC_MODE } from '@documenso/lib/constants/app';
|
|
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';
|
|
import { getRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
|
|
import type React from 'react';
|
|
import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
|
import { useSearchParams } from 'react-router';
|
|
|
|
import type { TDocumentEmailSettings } from '../../types/document-email';
|
|
import { formatDocumentsPath, formatTemplatesPath } from '../../utils/teams';
|
|
import type { TLocalField } from '../hooks/use-editor-fields';
|
|
import { useEditorFields } from '../hooks/use-editor-fields';
|
|
import { useEditorRecipients } from '../hooks/use-editor-recipients';
|
|
import { useEnvelopeAutosave } from '../hooks/use-envelope-autosave';
|
|
|
|
export type EnvelopeEditorStep = 'upload' | 'addFields' | 'preview';
|
|
|
|
type UpdateEnvelopePayload = Pick<TUpdateEnvelopeRequest, 'data' | 'meta'>;
|
|
|
|
type EnvelopeEditorProviderValue = {
|
|
editorConfig: EnvelopeEditorConfig;
|
|
|
|
envelope: TEditorEnvelope;
|
|
|
|
isEmbedded: boolean;
|
|
isDocument: boolean;
|
|
isTemplate: boolean;
|
|
/**
|
|
* Whether the instance is running in CSC (Cloud Signature Consortium) mode.
|
|
* Components can branch on this for any additional CSC-only UI gating
|
|
* beyond the overrides already baked into `editorConfig`.
|
|
*/
|
|
isCscMode: boolean;
|
|
|
|
setLocalEnvelope: (localEnvelope: Partial<TEditorEnvelope>) => void;
|
|
updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void;
|
|
updateEnvelopeAsync: (envelopeUpdates: UpdateEnvelopePayload) => Promise<void>;
|
|
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
|
|
setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>;
|
|
|
|
getRecipientColorKey: (recipientId: number) => TRecipientColor;
|
|
|
|
editorFields: ReturnType<typeof useEditorFields>;
|
|
editorRecipients: ReturnType<typeof useEditorRecipients>;
|
|
|
|
isAutosaving: boolean;
|
|
flushAutosave: () => Promise<TEditorEnvelope>;
|
|
autosaveError: boolean;
|
|
resetForms: () => void;
|
|
|
|
relativePath: {
|
|
basePath: string;
|
|
envelopePath: string;
|
|
editorPath: string;
|
|
documentRootPath: string;
|
|
templateRootPath: string;
|
|
};
|
|
|
|
navigateToStep: (step: EnvelopeEditorStep) => void;
|
|
syncEnvelope: () => Promise<void>;
|
|
|
|
registerExternalFlush: (key: string, flush: () => Promise<void>) => () => void;
|
|
registerPendingMutation: (promise: Promise<unknown>) => void;
|
|
|
|
organisationEmails?: { id: string; email: string }[];
|
|
};
|
|
|
|
interface EnvelopeEditorProviderProps {
|
|
children: React.ReactNode;
|
|
editorConfig?: EnvelopeEditorConfig;
|
|
initialEnvelope: TEditorEnvelope;
|
|
organisationEmails?: { id: string; email: string }[];
|
|
}
|
|
|
|
const EnvelopeEditorContext = createContext<EnvelopeEditorProviderValue | null>(null);
|
|
|
|
export const useCurrentEnvelopeEditor = () => {
|
|
const context = useContext(EnvelopeEditorContext);
|
|
|
|
if (!context) {
|
|
throw new Error('useCurrentEnvelopeEditor must be used within a EnvelopeEditorProvider');
|
|
}
|
|
|
|
return context;
|
|
};
|
|
|
|
export const EnvelopeEditorProvider = ({
|
|
children,
|
|
editorConfig: providedEditorConfig = DEFAULT_EDITOR_CONFIG,
|
|
initialEnvelope,
|
|
organisationEmails,
|
|
}: EnvelopeEditorProviderProps) => {
|
|
const { t } = useLingui();
|
|
const { toast } = useToast();
|
|
|
|
const [_searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const [envelope, _setEnvelope] = useState(initialEnvelope);
|
|
const [autosaveError, setAutosaveError] = useState<boolean>(false);
|
|
|
|
const isCscMode = IS_INSTANCE_CSC_MODE();
|
|
|
|
/**
|
|
* CSC-mode overrides applied on top of any caller-supplied editor config.
|
|
* TSP envelopes are forced SEQUENTIAL at send-time and the sign path has no
|
|
* nextSigner dictation; the assistant role's pre-fill semantics don't map
|
|
* onto each recipient signing their own complete PDF state. Hide all three
|
|
* up-front so authors don't pick options that would get silently coerced.
|
|
*/
|
|
const editorConfig = useMemo<EnvelopeEditorConfig>(() => {
|
|
if (!isCscMode || !providedEditorConfig.recipients) {
|
|
return providedEditorConfig;
|
|
}
|
|
|
|
return {
|
|
...providedEditorConfig,
|
|
recipients: {
|
|
...providedEditorConfig.recipients,
|
|
allowConfigureSigningOrder: false,
|
|
allowConfigureDictateNextSigner: false,
|
|
allowAssistantRole: false,
|
|
},
|
|
};
|
|
}, [isCscMode, providedEditorConfig]);
|
|
|
|
const envelopeRef = useRef(initialEnvelope);
|
|
|
|
const externalFlushCallbacksRef = useRef<Map<string, () => Promise<void>>>(new Map());
|
|
const pendingMutationsRef = useRef<Set<Promise<unknown>>>(new Set());
|
|
|
|
const registerExternalFlush = useCallback((key: string, flush: () => Promise<void>) => {
|
|
externalFlushCallbacksRef.current.set(key, flush);
|
|
|
|
return () => {
|
|
externalFlushCallbacksRef.current.delete(key);
|
|
};
|
|
}, []);
|
|
|
|
const registerPendingMutation = useCallback((promise: Promise<unknown>) => {
|
|
pendingMutationsRef.current.add(promise);
|
|
|
|
void promise.finally(() => {
|
|
pendingMutationsRef.current.delete(promise);
|
|
});
|
|
}, []);
|
|
|
|
const setEnvelope: typeof _setEnvelope = (action) => {
|
|
_setEnvelope((prev) => {
|
|
const next = typeof action === 'function' ? action(prev) : action;
|
|
envelopeRef.current = next;
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const isEmbedded = editorConfig.embedded !== undefined;
|
|
|
|
const editorFields = useEditorFields({
|
|
envelope,
|
|
handleFieldsUpdate: (fields) => setFieldsDebounced(fields),
|
|
});
|
|
|
|
const editorRecipients = useEditorRecipients({
|
|
envelope,
|
|
});
|
|
|
|
const setRecipientsMutation = trpc.envelope.recipient.set.useMutation();
|
|
const setFieldsMutation = trpc.envelope.field.set.useMutation();
|
|
const updateEnvelopeMutation = trpc.envelope.update.useMutation();
|
|
|
|
/**
|
|
* 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'] = [];
|
|
|
|
if (!isEmbedded) {
|
|
const response = await setRecipientsMutation.mutateAsync({
|
|
envelopeId: envelope.id,
|
|
envelopeType: envelope.type,
|
|
recipients: localRecipients,
|
|
});
|
|
|
|
recipients = response.data;
|
|
} else {
|
|
recipients = mapLocalRecipientsToRecipients({ envelope, localRecipients });
|
|
}
|
|
|
|
setEnvelope((prev) => ({
|
|
...prev,
|
|
recipients,
|
|
fields: prev.fields.filter((field) => recipients.some((recipient) => recipient.id === field.recipientId)),
|
|
}));
|
|
|
|
// Reset the local fields to ensure deleted recipient fields are removed.
|
|
editorFields.resetForm(
|
|
envelope.fields.filter((field) => recipients.some((recipient) => recipient.id === field.recipientId)),
|
|
);
|
|
|
|
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 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: flushSetFields,
|
|
isPending: isFieldsMutationPending,
|
|
} = useEnvelopeAutosave(async (localFields: TLocalField[]) => {
|
|
try {
|
|
let fields: TSetEnvelopeFieldsResponse['data'] = [];
|
|
|
|
if (!isEmbedded) {
|
|
const response = await setFieldsMutation.mutateAsync({
|
|
envelopeId: envelope.id,
|
|
envelopeType: envelope.type,
|
|
fields: localFields,
|
|
});
|
|
|
|
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: updateEnvelopeDebounced,
|
|
flush: flushUpdateEnvelope,
|
|
isPending: isEnvelopeMutationPending,
|
|
} = 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) => ({
|
|
...prev,
|
|
...envelopeUpdates.data,
|
|
meta: {
|
|
...prev.documentMeta,
|
|
...envelopeUpdates.meta,
|
|
},
|
|
}));
|
|
|
|
updateEnvelopeDebounced(envelopeUpdates);
|
|
};
|
|
|
|
const getRecipientColorKey = useCallback(
|
|
(recipientId: number) => getRecipientColor(envelope.recipients.findIndex((r) => r.id === recipientId)),
|
|
[envelope.recipients],
|
|
);
|
|
|
|
const { refetch: reloadEnvelope } = trpc.envelope.editor.get.useQuery(
|
|
{
|
|
envelopeId: envelope.id,
|
|
},
|
|
{
|
|
enabled: !isEmbedded,
|
|
gcTime: 0,
|
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Fetch and sync the envelope back into the editor.
|
|
*
|
|
* Overrides everything.
|
|
*/
|
|
const syncEnvelope = async () => {
|
|
await flushAutosave();
|
|
|
|
// Bypass syncing for embedded mode.
|
|
if (isEmbedded) {
|
|
return;
|
|
}
|
|
|
|
const fetchedEnvelopeData = await reloadEnvelope();
|
|
|
|
if (fetchedEnvelopeData.data) {
|
|
setEnvelope(fetchedEnvelopeData.data);
|
|
|
|
editorRecipients.resetForm({
|
|
recipients: fetchedEnvelopeData.data.recipients,
|
|
documentMeta: fetchedEnvelopeData.data.documentMeta,
|
|
});
|
|
|
|
editorFields.resetForm(fetchedEnvelopeData.data.fields);
|
|
}
|
|
};
|
|
|
|
const setLocalEnvelope = (localEnvelope: Partial<TEditorEnvelope>) => {
|
|
setEnvelope((prev) => ({ ...prev, ...localEnvelope }));
|
|
};
|
|
|
|
const isAutosaving = useMemo(() => {
|
|
return isFieldsMutationPending || isRecipientsMutationPending || isEnvelopeMutationPending;
|
|
}, [isFieldsMutationPending, isRecipientsMutationPending, isEnvelopeMutationPending]);
|
|
|
|
const relativePath = useMemo(() => {
|
|
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.embedded) {
|
|
let embeddedEditorPath =
|
|
editorConfig.embedded.mode === 'edit'
|
|
? `/embed/v2/authoring/envelope/edit/${envelope.id}`
|
|
: `/embed/v2/authoring/envelope/create`;
|
|
|
|
embeddedEditorPath += `?token=${editorConfig.embedded.presignToken}`;
|
|
|
|
envelopePath = embeddedEditorPath;
|
|
editorPath = embeddedEditorPath;
|
|
documentRootPath = embeddedEditorPath;
|
|
templateRootPath = embeddedEditorPath;
|
|
}
|
|
|
|
return {
|
|
basePath,
|
|
envelopePath,
|
|
editorPath,
|
|
documentRootPath,
|
|
templateRootPath,
|
|
};
|
|
}, [envelope.type, envelope.id]);
|
|
|
|
const navigateToStep = (step: EnvelopeEditorStep) => {
|
|
setSearchParams((prev) => {
|
|
const newParams = new URLSearchParams(prev);
|
|
|
|
if (step === 'upload') {
|
|
newParams.delete('step');
|
|
} else {
|
|
newParams.set('step', step);
|
|
}
|
|
|
|
return newParams;
|
|
});
|
|
};
|
|
|
|
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()]);
|
|
|
|
// Flush all registered external flushes (e.g., upload page's debounced item updates).
|
|
const externalFlushes = Array.from(externalFlushCallbacksRef.current.values());
|
|
await Promise.all(externalFlushes.map(async (flush) => flush()));
|
|
|
|
// Await all registered pending mutations (e.g., in-flight creates/deletes).
|
|
// Use allSettled so a single failed mutation doesn't prevent awaiting the rest.
|
|
if (pendingMutationsRef.current.size > 0) {
|
|
await Promise.allSettled(Array.from(pendingMutationsRef.current));
|
|
}
|
|
|
|
return envelopeRef.current;
|
|
};
|
|
|
|
return (
|
|
<EnvelopeEditorContext.Provider
|
|
value={{
|
|
editorConfig,
|
|
envelope,
|
|
isEmbedded,
|
|
isDocument: envelope.type === EnvelopeType.DOCUMENT,
|
|
isTemplate: envelope.type === EnvelopeType.TEMPLATE,
|
|
isCscMode,
|
|
setLocalEnvelope,
|
|
getRecipientColorKey,
|
|
updateEnvelope,
|
|
updateEnvelopeAsync,
|
|
setRecipientsDebounced,
|
|
setRecipientsAsync,
|
|
editorFields,
|
|
editorRecipients,
|
|
autosaveError,
|
|
flushAutosave,
|
|
isAutosaving,
|
|
relativePath,
|
|
syncEnvelope,
|
|
navigateToStep,
|
|
resetForms,
|
|
registerExternalFlush,
|
|
registerPendingMutation,
|
|
organisationEmails,
|
|
}}
|
|
>
|
|
{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((r) => r.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,
|
|
expiresAt: foundRecipient?.expiresAt || null,
|
|
expirationNotifiedAt: foundRecipient?.expirationNotifiedAt || null,
|
|
};
|
|
});
|
|
};
|
|
|
|
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,
|
|
};
|
|
});
|
|
};
|