mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 17:21:41 +10:00
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:
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|||||||
@ -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,15 +481,17 @@ export const ConfigureFieldsView = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex gap-2">
|
<div className="mt-6 flex gap-2">
|
||||||
<Button
|
{onBack && (
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
className="flex-1"
|
variant="ghost"
|
||||||
loading={form.formState.isSubmitting}
|
className="flex-1"
|
||||||
onClick={() => onBack(form.getValues())}
|
loading={form.formState.isSubmitting}
|
||||||
>
|
onClick={() => onBack(form.getValues())}
|
||||||
<Trans>Back</Trans>
|
>
|
||||||
</Button>
|
<Trans>Back</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@ -642,15 +644,17 @@ export const ConfigureFieldsView = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 flex gap-2">
|
<div className="mt-6 flex gap-2">
|
||||||
<Button
|
{onBack && (
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
className="flex-1"
|
variant="ghost"
|
||||||
loading={form.formState.isSubmitting}
|
className="flex-1"
|
||||||
onClick={() => onBack(form.getValues())}
|
loading={form.formState.isSubmitting}
|
||||||
>
|
onClick={() => onBack(form.getValues())}
|
||||||
<Trans>Back</Trans>
|
>
|
||||||
</Button>
|
<Trans>Back</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -2,21 +2,24 @@ 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({
|
allowConfigureSignatureTypes: z.boolean().optional(),
|
||||||
allowConfigureSignatureTypes: z.boolean().optional(),
|
allowConfigureLanguage: z.boolean().optional(),
|
||||||
allowConfigureLanguage: z.boolean().optional(),
|
allowConfigureDateFormat: z.boolean().optional(),
|
||||||
allowConfigureDateFormat: z.boolean().optional(),
|
allowConfigureTimezone: z.boolean().optional(),
|
||||||
allowConfigureTimezone: z.boolean().optional(),
|
allowConfigureRedirectUrl: z.boolean().optional(),
|
||||||
allowConfigureRedirectUrl: z.boolean().optional(),
|
allowConfigureCommunication: z.boolean().optional(),
|
||||||
allowConfigureCommunication: z.boolean().optional(),
|
})
|
||||||
})
|
.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>;
|
||||||
|
|||||||
@ -21,33 +21,38 @@ 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(),
|
||||||
fields: ZFieldAndMetaSchema.and(
|
email: z.string().email(),
|
||||||
z.object({
|
name: z.string(),
|
||||||
pageNumber: ZFieldPageNumberSchema,
|
role: z.nativeEnum(RecipientRole),
|
||||||
pageX: ZFieldPageXSchema,
|
signingOrder: z.number().optional(),
|
||||||
pageY: ZFieldPageYSchema,
|
// We have an any cast so any changes here you need to update it in the embeding document edit page
|
||||||
width: ZFieldWidthSchema,
|
// Search: "map<any>" to find it
|
||||||
height: ZFieldHeightSchema,
|
fields: ZFieldAndMetaSchema.and(
|
||||||
}),
|
z.object({
|
||||||
)
|
id: z.number().optional(),
|
||||||
.array()
|
pageNumber: ZFieldPageNumberSchema,
|
||||||
.optional(),
|
pageX: ZFieldPageXSchema,
|
||||||
}),
|
pageY: ZFieldPageYSchema,
|
||||||
)
|
width: ZFieldWidthSchema,
|
||||||
|
height: ZFieldHeightSchema,
|
||||||
.optional(),
|
}),
|
||||||
|
)
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
meta: z
|
meta: z
|
||||||
.object({
|
.object({
|
||||||
subject: ZDocumentMetaSubjectSchema.optional(),
|
subject: ZDocumentMetaSubjectSchema.optional(),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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,
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user