fix: embed editing updates (#2197)

Allows empty recipients for embed template authoring.

Also allows fixing the step to editing fields only for embedded
authoring updates.
This commit is contained in:
Lucas Smith
2025-11-15 00:47:50 +11:00
committed by GitHub
parent dabd2564cd
commit de3e6d2115
16 changed files with 208 additions and 151 deletions

View File

@ -81,33 +81,6 @@ export const ConfigureDocumentAdvancedSettings = ({
<TabsContent value="general" className="mt-0"> <TabsContent value="general" className="mt-0">
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
{/* <FormField
control={control}
name="meta.externalId"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>External ID</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<Trans>
Add an external ID to the document. This can be used to identify the
document in external systems.
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} disabled={isSubmitting} />
</FormControl>
<FormMessage />
</FormItem>
)}
/> */}
{features.allowConfigureSignatureTypes && ( {features.allowConfigureSignatureTypes && (
<FormField <FormField
control={control} control={control}

View File

@ -66,14 +66,13 @@ export const ConfigureDocumentRecipients = ({
}); });
const onAddSigner = useCallback(() => { const onAddSigner = useCallback(() => {
const signerNumber = signers.length + 1;
const recipientSigningOrder = const recipientSigningOrder =
signers.length > 0 ? (signers[signers.length - 1]?.signingOrder || 0) + 1 : 1; signers.length > 0 ? (signers[signers.length - 1]?.signingOrder || 0) + 1 : 1;
appendSigner({ appendSigner({
formId: nanoid(8), formId: nanoid(8),
name: isTemplate ? `Recipient ${signerNumber}` : '', name: '',
email: isTemplate ? `recipient.${signerNumber}@document.com` : '', email: '',
role: RecipientRole.SIGNER, role: RecipientRole.SIGNER,
signingOrder: signingOrder:
signingOrder === DocumentSigningOrder.SEQUENTIAL ? recipientSigningOrder : undefined, signingOrder === DocumentSigningOrder.SEQUENTIAL ? recipientSigningOrder : undefined,

View File

@ -25,9 +25,11 @@ import { ConfigureDocumentUpload } from './configure-document-upload';
import { import {
type TConfigureEmbedFormSchema, type TConfigureEmbedFormSchema,
ZConfigureEmbedFormSchema, ZConfigureEmbedFormSchema,
ZConfigureTemplateEmbedFormSchema,
} from './configure-document-view.types'; } from './configure-document-view.types';
export interface ConfigureDocumentViewProps { export interface ConfigureDocumentViewProps {
type?: 'document' | 'template';
onSubmit: (data: TConfigureEmbedFormSchema) => void | Promise<void>; onSubmit: (data: TConfigureEmbedFormSchema) => void | Promise<void>;
defaultValues?: Partial<TConfigureEmbedFormSchema>; defaultValues?: Partial<TConfigureEmbedFormSchema>;
disableUpload?: boolean; disableUpload?: boolean;
@ -35,6 +37,7 @@ export interface ConfigureDocumentViewProps {
} }
export const ConfigureDocumentView = ({ export const ConfigureDocumentView = ({
type = 'document',
onSubmit, onSubmit,
defaultValues, defaultValues,
disableUpload, disableUpload,
@ -42,14 +45,16 @@ export const ConfigureDocumentView = ({
const { isTemplate } = useConfigureDocument(); const { isTemplate } = useConfigureDocument();
const form = useForm<TConfigureEmbedFormSchema>({ const form = useForm<TConfigureEmbedFormSchema>({
resolver: zodResolver(ZConfigureEmbedFormSchema), resolver: zodResolver(
type === 'template' ? ZConfigureTemplateEmbedFormSchema : ZConfigureEmbedFormSchema,
),
defaultValues: { defaultValues: {
title: defaultValues?.title || '', title: defaultValues?.title || '',
signers: defaultValues?.signers || [ signers: defaultValues?.signers || [
{ {
formId: nanoid(8), formId: nanoid(8),
name: isTemplate ? `Recipient ${1}` : '', name: '',
email: isTemplate ? `recipient.${1}@document.com` : '', email: '',
role: RecipientRole.SIGNER, role: RecipientRole.SIGNER,
signingOrder: 1, signingOrder: 1,
disabled: false, disabled: false,

View File

@ -17,7 +17,7 @@ export const ZConfigureEmbedFormSchema = z.object({
z.object({ z.object({
nativeId: z.number().optional(), nativeId: z.number().optional(),
formId: z.string(), formId: z.string(),
name: z.string().min(1, { message: 'Name is required' }), name: z.string(),
email: z.string().email('Invalid email address'), email: z.string().email('Invalid email address'),
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']), role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
signingOrder: z.number().optional(), signingOrder: z.number().optional(),
@ -48,3 +48,17 @@ export const ZConfigureEmbedFormSchema = z.object({
}) })
.optional(), .optional(),
}); });
export const ZConfigureTemplateEmbedFormSchema = ZConfigureEmbedFormSchema.extend({
signers: z.array(
z.object({
nativeId: z.number().optional(),
formId: z.string(),
name: z.string(),
email: z.union([z.string().length(0), z.string().email('Invalid email address')]),
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
signingOrder: z.number().optional(),
disabled: z.boolean().optional(),
}),
),
});

View File

@ -42,7 +42,7 @@ export type ConfigureFieldsViewProps = {
configData: TConfigureEmbedFormSchema; configData: TConfigureEmbedFormSchema;
documentData?: DocumentData; documentData?: DocumentData;
defaultValues?: Partial<TConfigureFieldsFormSchema>; defaultValues?: Partial<TConfigureFieldsFormSchema>;
onBack: (data: TConfigureFieldsFormSchema) => void; onBack?: (data: TConfigureFieldsFormSchema) => void;
onSubmit: (data: TConfigureFieldsFormSchema) => void; onSubmit: (data: TConfigureFieldsFormSchema) => void;
}; };
@ -481,6 +481,7 @@ export const ConfigureFieldsView = ({
</div> </div>
<div className="mt-6 flex gap-2"> <div className="mt-6 flex gap-2">
{onBack && (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@ -490,6 +491,7 @@ export const ConfigureFieldsView = ({
> >
<Trans>Back</Trans> <Trans>Back</Trans>
</Button> </Button>
)}
<Button <Button
className="flex-1" className="flex-1"
@ -642,6 +644,7 @@ export const ConfigureFieldsView = ({
</div> </div>
<div className="mt-6 flex gap-2"> <div className="mt-6 flex gap-2">
{onBack && (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@ -651,6 +654,7 @@ export const ConfigureFieldsView = ({
> >
<Trans>Back</Trans> <Trans>Back</Trans>
</Button> </Button>
)}
<Button <Button
className="flex-1" className="flex-1"

View File

@ -36,14 +36,14 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
} }
return { return {
hasValidToken: !!result,
token, token,
hasValidToken: !!result,
allowEmbedAuthoringWhiteLabel, allowEmbedAuthoringWhiteLabel,
}; };
}; };
export default function AuthoringLayout() { export default function AuthoringLayout() {
const { hasValidToken, token, allowEmbedAuthoringWhiteLabel } = useLoaderData<typeof loader>(); const { token, hasValidToken, allowEmbedAuthoringWhiteLabel } = useLoaderData<typeof loader>();
useLayoutEffect(() => { useLayoutEffect(() => {
try { try {

View File

@ -27,7 +27,7 @@ import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fiel
import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types'; import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types';
import { import {
type TBaseEmbedAuthoringSchema, type TBaseEmbedAuthoringSchema,
ZBaseEmbedAuthoringSchema, ZBaseEmbedAuthoringEditSchema,
} from '~/types/embed-authoring-base-schema'; } from '~/types/embed-authoring-base-schema';
import type { Route } from './+types/document.edit.$id'; import type { Route } from './+types/document.edit.$id';
@ -88,6 +88,8 @@ export default function EmbeddingAuthoringDocumentEditPage() {
const { document } = useLoaderData<typeof loader>(); const { document } = useLoaderData<typeof loader>();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const signatureTypes = useMemo(() => { const signatureTypes = useMemo(() => {
const types: string[] = []; const types: string[] = [];
@ -159,6 +161,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null); const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null);
const [externalId, setExternalId] = useState<string | null>(null); const [externalId, setExternalId] = useState<string | null>(null);
const [currentStep, setCurrentStep] = useState(1); const [currentStep, setCurrentStep] = useState(1);
const [canGoBack, setCanGoBack] = useState(true);
const { mutateAsync: updateEmbeddingDocument } = const { mutateAsync: updateEmbeddingDocument } =
trpc.embeddingPresign.updateEmbeddingDocument.useMutation(); trpc.embeddingPresign.updateEmbeddingDocument.useMutation();
@ -177,6 +180,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
fields: fieldData.fields.filter((field) => signerEmails.includes(field.signerEmail)), fields: fieldData.fields.filter((field) => signerEmails.includes(field.signerEmail)),
}; };
}); });
setCurrentStep(2); setCurrentStep(2);
}; };
@ -275,7 +279,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
try { try {
const hash = window.location.hash.slice(1); const hash = window.location.hash.slice(1);
const result = ZBaseEmbedAuthoringSchema.safeParse( const result = ZBaseEmbedAuthoringEditSchema.safeParse(
JSON.parse(decodeURIComponent(atob(hash))), JSON.parse(decodeURIComponent(atob(hash))),
); );
@ -285,15 +289,26 @@ export default function EmbeddingAuthoringDocumentEditPage() {
setFeatures(result.data.features); setFeatures(result.data.features);
if (result.data.onlyEditFields) {
setCurrentStep(2);
setCanGoBack(false);
}
// Extract externalId from the parsed data if available // Extract externalId from the parsed data if available
if (result.data.externalId) { if (result.data.externalId) {
setExternalId(result.data.externalId); setExternalId(result.data.externalId);
} }
setHasFinishedInit(true);
} catch (err) { } catch (err) {
console.error('Error parsing embedding params:', err); console.error('Error parsing embedding params:', err);
} }
}, []); }, []);
if (!hasFinishedInit) {
return null;
}
return ( return (
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg p-6"> <div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg p-6">
<ConfigureDocumentProvider isTemplate={false} features={features ?? {}}> <ConfigureDocumentProvider isTemplate={false} features={features ?? {}}>
@ -308,7 +323,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
configData={configuration!} configData={configuration!}
documentData={document.documentData} documentData={document.documentData}
defaultValues={fields ?? undefined} defaultValues={fields ?? undefined}
onBack={handleBackToConfig} onBack={canGoBack ? handleBackToConfig : undefined}
onSubmit={handleConfigureFieldsSubmit} onSubmit={handleConfigureFieldsSubmit}
/> />
</Stepper> </Stepper>

View File

@ -154,6 +154,7 @@ export default function EmbeddingAuthoringTemplateCreatePage() {
<ConfigureDocumentProvider isTemplate={true} features={features ?? {}}> <ConfigureDocumentProvider isTemplate={true} features={features ?? {}}>
<Stepper currentStep={currentStep} setCurrentStep={setCurrentStep}> <Stepper currentStep={currentStep} setCurrentStep={setCurrentStep}>
<ConfigureDocumentView <ConfigureDocumentView
type="template"
defaultValues={configuration ?? undefined} defaultValues={configuration ?? undefined}
onSubmit={handleConfigurePageViewSubmit} onSubmit={handleConfigurePageViewSubmit}
/> />

View File

@ -27,7 +27,7 @@ import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fiel
import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types'; import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types';
import { import {
type TBaseEmbedAuthoringSchema, type TBaseEmbedAuthoringSchema,
ZBaseEmbedAuthoringSchema, ZBaseEmbedAuthoringEditSchema,
} from '~/types/embed-authoring-base-schema'; } from '~/types/embed-authoring-base-schema';
import type { Route } from './+types/document.edit.$id'; import type { Route } from './+types/document.edit.$id';
@ -88,6 +88,8 @@ export default function EmbeddingAuthoringTemplateEditPage() {
const { template } = useLoaderData<typeof loader>(); const { template } = useLoaderData<typeof loader>();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const signatureTypes = useMemo(() => { const signatureTypes = useMemo(() => {
const types: string[] = []; const types: string[] = [];
@ -159,6 +161,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null); const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null);
const [externalId, setExternalId] = useState<string | null>(null); const [externalId, setExternalId] = useState<string | null>(null);
const [currentStep, setCurrentStep] = useState(1); const [currentStep, setCurrentStep] = useState(1);
const [canGoBack, setCanGoBack] = useState(true);
const { mutateAsync: updateEmbeddingTemplate } = const { mutateAsync: updateEmbeddingTemplate } =
trpc.embeddingPresign.updateEmbeddingTemplate.useMutation(); trpc.embeddingPresign.updateEmbeddingTemplate.useMutation();
@ -230,7 +233,9 @@ export default function EmbeddingAuthoringTemplateEditPage() {
signingOrder: signer.signingOrder, signingOrder: signer.signingOrder,
fields: fields fields: fields
.filter((field) => field.signerEmail === signer.email) .filter((field) => field.signerEmail === signer.email)
.map((f) => ({ // There's a gnarly discriminated union that makes this hard to satisfy, we're casting for the second
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map<any>((f) => ({
...f, ...f,
id: f.nativeId, id: f.nativeId,
envelopeItemId: template.templateDocumentData.envelopeItemId, envelopeItemId: template.templateDocumentData.envelopeItemId,
@ -273,7 +278,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
try { try {
const hash = window.location.hash.slice(1); const hash = window.location.hash.slice(1);
const result = ZBaseEmbedAuthoringSchema.safeParse( const result = ZBaseEmbedAuthoringEditSchema.safeParse(
JSON.parse(decodeURIComponent(atob(hash))), JSON.parse(decodeURIComponent(atob(hash))),
); );
@ -283,20 +288,32 @@ export default function EmbeddingAuthoringTemplateEditPage() {
setFeatures(result.data.features); setFeatures(result.data.features);
if (result.data.onlyEditFields) {
setCurrentStep(2);
setCanGoBack(false);
}
// Extract externalId from the parsed data if available // Extract externalId from the parsed data if available
if (result.data.externalId) { if (result.data.externalId) {
setExternalId(result.data.externalId); setExternalId(result.data.externalId);
} }
setHasFinishedInit(true);
} catch (err) { } catch (err) {
console.error('Error parsing embedding params:', err); console.error('Error parsing embedding params:', err);
} }
}, []); }, []);
if (!hasFinishedInit) {
return null;
}
return ( return (
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg p-6"> <div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg p-6">
<ConfigureDocumentProvider isTemplate={false} features={features ?? {}}> <ConfigureDocumentProvider isTemplate={false} features={features ?? {}}>
<Stepper currentStep={currentStep} setCurrentStep={setCurrentStep}> <Stepper currentStep={currentStep} setCurrentStep={setCurrentStep}>
<ConfigureDocumentView <ConfigureDocumentView
type="template"
defaultValues={configuration ?? undefined} defaultValues={configuration ?? undefined}
disableUpload={true} disableUpload={true}
onSubmit={handleConfigurePageViewSubmit} onSubmit={handleConfigurePageViewSubmit}
@ -306,7 +323,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
configData={configuration!} configData={configuration!}
documentData={template.templateDocumentData} documentData={template.templateDocumentData}
defaultValues={fields ?? undefined} defaultValues={fields ?? undefined}
onBack={handleBackToConfig} onBack={canGoBack ? handleBackToConfig : undefined}
onSubmit={handleConfigureFieldsSubmit} onSubmit={handleConfigureFieldsSubmit}
/> />
</Stepper> </Stepper>

View File

@ -2,8 +2,7 @@ import { z } from 'zod';
import { ZBaseEmbedDataSchema } from './embed-base-schemas'; import { ZBaseEmbedDataSchema } from './embed-base-schemas';
export const ZBaseEmbedAuthoringSchema = z export const ZBaseEmbedAuthoringSchema = ZBaseEmbedDataSchema.extend({
.object({
externalId: z.string().optional(), externalId: z.string().optional(),
features: z features: z
.object({ .object({
@ -16,7 +15,11 @@ export const ZBaseEmbedAuthoringSchema = z
}) })
.optional() .optional()
.default({}), .default({}),
}) });
.and(ZBaseEmbedDataSchema);
export const ZBaseEmbedAuthoringEditSchema = ZBaseEmbedAuthoringSchema.extend({
onlyEditFields: z.boolean().optional().default(false),
});
export type TBaseEmbedAuthoringSchema = z.infer<typeof ZBaseEmbedAuthoringSchema>; export type TBaseEmbedAuthoringSchema = z.infer<typeof ZBaseEmbedAuthoringSchema>;
export type TBaseEmbedAuthoringEditSchema = z.infer<typeof ZBaseEmbedAuthoringEditSchema>;

View File

@ -21,20 +21,27 @@ import {
ZFieldWidthSchema, ZFieldWidthSchema,
} from '@documenso/lib/types/field'; } from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { RecipientRole } from '@documenso/prisma/client';
import { DocumentSigningOrder } from '@documenso/prisma/generated/types'; import { DocumentSigningOrder } from '@documenso/prisma/generated/types';
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from '../document-router/schema'; import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from '../document-router/schema';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
export const ZCreateEmbeddingDocumentRequestSchema = z.object({ export const ZCreateEmbeddingDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
documentDataId: z.string(), documentDataId: z.string(),
externalId: ZDocumentExternalIdSchema.optional(), externalId: ZDocumentExternalIdSchema.optional(),
recipients: z recipients: z.array(
.array( z.object({
ZCreateRecipientSchema.extend({ id: z.number().optional(),
email: z.string().email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().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( fields: ZFieldAndMetaSchema.and(
z.object({ z.object({
id: z.number().optional(),
pageNumber: ZFieldPageNumberSchema, pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema, pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema, pageY: ZFieldPageYSchema,
@ -45,9 +52,7 @@ export const ZCreateEmbeddingDocumentRequestSchema = z.object({
.array() .array()
.optional(), .optional(),
}), }),
) ),
.optional(),
meta: z meta: z
.object({ .object({
subject: ZDocumentMetaSubjectSchema.optional(), subject: ZDocumentMetaSubjectSchema.optional(),

View File

@ -1,4 +1,4 @@
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client'; import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod'; import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
@ -21,30 +21,33 @@ import {
ZFieldPageYSchema, ZFieldPageYSchema,
ZFieldWidthSchema, ZFieldWidthSchema,
} from '@documenso/lib/types/field'; } from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZDocumentTitleSchema } from '../document-router/schema'; import { ZDocumentTitleSchema } from '../document-router/schema';
const ZFieldSchema = z.object({
type: z.nativeEnum(FieldType),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
fieldMeta: ZFieldMetaSchema.optional(),
});
export const ZCreateEmbeddingTemplateRequestSchema = z.object({ export const ZCreateEmbeddingTemplateRequestSchema = z.object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
documentDataId: z.string(), documentDataId: z.string(),
recipients: z.array( recipients: z.array(
z.object({ z.object({
email: z.string().email(), email: z.union([z.string().length(0), z.string().email()]),
name: z.string().optional(), name: z.string(),
role: z.nativeEnum(RecipientRole).optional(), role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(), signingOrder: z.number().optional(),
fields: z.array(ZFieldSchema).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(
z.object({
id: z.number().optional(),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}), }),
), ),
meta: z meta: z

View File

@ -32,7 +32,7 @@ export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
recipients: z.array( recipients: z.array(
z.object({ z.object({
id: z.number().optional(), id: z.number().optional(),
email: z.string().toLowerCase().email().min(1), email: z.string().email(),
name: z.string(), name: z.string(),
role: z.nativeEnum(RecipientRole), role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(), signingOrder: z.number().optional(),

View File

@ -3,7 +3,6 @@ import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embeddin
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope'; import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template'; import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients'; import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
import { nanoid } from '@documenso/lib/universal/id';
import { procedure } from '../trpc'; import { procedure } from '../trpc';
import { import {
@ -53,11 +52,6 @@ export const updateEmbeddingTemplateRoute = procedure
requestMetadata: ctx.metadata, requestMetadata: ctx.metadata,
}); });
const recipientsWithClientId = recipients.map((recipient) => ({
...recipient,
clientId: nanoid(),
}));
const { recipients: updatedRecipients } = await setTemplateRecipients({ const { recipients: updatedRecipients } = await setTemplateRecipients({
userId: apiToken.userId, userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined, teamId: apiToken.teamId ?? undefined,
@ -65,7 +59,7 @@ export const updateEmbeddingTemplateRoute = procedure
type: 'templateId', type: 'templateId',
id: templateId, id: templateId,
}, },
recipients: recipientsWithClientId.map((recipient) => ({ recipients: recipients.map((recipient) => ({
id: recipient.id, id: recipient.id,
email: recipient.email, email: recipient.email,
name: recipient.name ?? '', name: recipient.name ?? '',
@ -74,8 +68,8 @@ export const updateEmbeddingTemplateRoute = procedure
})), })),
}); });
const fields = recipientsWithClientId.flatMap((recipient) => { const fields = recipients.flatMap((recipient) => {
const recipientId = updatedRecipients.find((r) => r.email === recipient.email)?.id; const recipientId = updatedRecipients.find((r) => r.id === recipient.id)?.id;
if (!recipientId) { if (!recipientId) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, { throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
@ -86,8 +80,6 @@ export const updateEmbeddingTemplateRoute = procedure
return (recipient.fields ?? []).map((field) => ({ return (recipient.fields ?? []).map((field) => ({
...field, ...field,
recipientId, recipientId,
// !: Temp property to be removed once we don't link based on signer email
signerEmail: recipient.email,
})); }));
}); });

View File

@ -21,7 +21,7 @@ import {
ZFieldPageYSchema, ZFieldPageYSchema,
ZFieldWidthSchema, ZFieldWidthSchema,
} from '@documenso/lib/types/field'; } from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldAndMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZDocumentTitleSchema } from '../document-router/schema'; import { ZDocumentTitleSchema } from '../document-router/schema';
@ -44,11 +44,25 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
recipients: z.array( recipients: z.array(
z.object({ z.object({
id: z.number().optional(), id: z.number().optional(),
email: z.string().email(), email: z.union([z.string().length(0), z.string().email()]),
name: z.string().optional(), name: z.string(),
role: z.nativeEnum(RecipientRole).optional(), role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(), signingOrder: z.number().optional(),
fields: z.array(ZFieldSchema).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(
z.object({
id: z.number().optional(),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
envelopeItemId: z.string(),
}),
)
.array()
.optional(),
}), }),
), ),
meta: z meta: z

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
@ -34,8 +34,8 @@ export const RecipientSelector = ({
const { _ } = useLingui(); const { _ } = useLingui();
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false); const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const recipientsByRole = useCallback(() => { const recipientsByRole = useMemo(() => {
const recipientsByRole: Record<RecipientRole, Recipient[]> = { const recipientsWithRole: Record<RecipientRole, Recipient[]> = {
CC: [], CC: [],
VIEWER: [], VIEWER: [],
SIGNER: [], SIGNER: [],
@ -44,14 +44,14 @@ export const RecipientSelector = ({
}; };
recipients.forEach((recipient) => { recipients.forEach((recipient) => {
recipientsByRole[recipient.role].push(recipient); recipientsWithRole[recipient.role].push(recipient);
}); });
return recipientsByRole; return recipientsWithRole;
}, [recipients]); }, [recipients]);
const recipientsByRoleToDisplay = useCallback(() => { const recipientsByRoleToDisplay = useMemo(() => {
return Object.entries(recipientsByRole()) return Object.entries(recipientsByRole)
.filter( .filter(
([role]) => ([role]) =>
role !== RecipientRole.CC && role !== RecipientRole.CC &&
@ -71,6 +71,28 @@ export const RecipientSelector = ({
); );
}, [recipientsByRole]); }, [recipientsByRole]);
const getRecipientLabel = useCallback(
(recipient: Recipient) => {
if (recipient.name && recipient.email) {
return `${recipient.name} (${recipient.email})`;
}
if (recipient.name) {
return recipient.name;
}
if (recipient.email) {
return recipient.email;
}
// Since objects are basically pointers we can use `indexOf` rather than `findIndex`
const index = recipients.indexOf(recipient);
return `Recipient ${index + 1}`;
},
[recipients, selectedRecipient],
);
return ( return (
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}> <Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -89,16 +111,12 @@ export const RecipientSelector = ({
className, className,
)} )}
> >
{selectedRecipient?.email && ( {selectedRecipient && (
<span className="flex-1 truncate text-left"> <span className="flex-1 truncate text-left">
{selectedRecipient?.name} ({selectedRecipient?.email}) {getRecipientLabel(selectedRecipient)}
</span> </span>
)} )}
{!selectedRecipient?.email && (
<span className="flex-1 truncate text-left">{selectedRecipient?.email}</span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4" /> <ChevronsUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@ -113,7 +131,7 @@ export const RecipientSelector = ({
</span> </span>
</CommandEmpty> </CommandEmpty>
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => ( {recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<CommandGroup key={roleIndex}> <CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium"> <div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)} {_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
@ -154,13 +172,7 @@ export const RecipientSelector = ({
'text-foreground/80': recipient.id === selectedRecipient?.id, 'text-foreground/80': recipient.id === selectedRecipient?.id,
})} })}
> >
{recipient.name && ( {getRecipientLabel(recipient)}
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && <span title={recipient.email}>{recipient.email}</span>}
</span> </span>
<div className="ml-auto flex items-center justify-center"> <div className="ml-auto flex items-center justify-center">