feat: add support for attachments in template management

- Enhanced TemplateEditForm to include attachments in the template data.
- Updated createDocumentFromTemplate to handle attachment creation.
- Modified updateTemplate to manage attachment updates and deletions.
- Integrated attachments into ZTemplateSchema and ZAddTemplateSettingsFormSchema for validation.
- Improved getTemplateById to fetch attachments alongside other template data.
This commit is contained in:
Catalin Documenso
2025-05-06 15:48:52 +03:00
parent d10ec437cf
commit cec25ac719
9 changed files with 94 additions and 10 deletions

View File

@ -136,6 +136,7 @@ export const TemplateEditForm = ({
visibility: data.visibility, visibility: data.visibility,
globalAccessAuth: data.globalAccessAuth ?? null, globalAccessAuth: data.globalAccessAuth ?? null,
globalActionAuth: data.globalActionAuth ?? null, globalActionAuth: data.globalActionAuth ?? null,
attachments: data.attachments ?? [],
}, },
meta: { meta: {
...data.meta, ...data.meta,
@ -165,6 +166,14 @@ export const TemplateEditForm = ({
await Promise.all([ await Promise.all([
updateTemplateSettings({ updateTemplateSettings({
templateId: template.id, templateId: template.id,
data: {
attachments: template.attachments?.map((attachment) => ({
id: attachment.id,
label: attachment.label,
url: attachment.url,
formId: attachment.id,
})),
},
meta: { meta: {
signingOrder: data.signingOrder, signingOrder: data.signingOrder,
allowDictateNextSigner: data.allowDictateNextSigner, allowDictateNextSigner: data.allowDictateNextSigner,

View File

@ -296,6 +296,7 @@ export const createDocumentFromTemplate = async ({
fields: true, fields: true,
}, },
}, },
attachments: true,
templateDocumentData: true, templateDocumentData: true,
templateMeta: true, templateMeta: true,
team: { team: {
@ -385,6 +386,15 @@ export const createDocumentFromTemplate = async ({
}), }),
visibility: template.visibility || template.team?.teamGlobalSettings?.documentVisibility, visibility: template.visibility || template.team?.teamGlobalSettings?.documentVisibility,
useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false, useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false,
attachments: {
create: template.attachments.map((attachment) => ({
type: attachment.type,
label: attachment.label,
url: attachment.url,
createdAt: attachment.createdAt,
updatedAt: attachment.updatedAt,
})),
},
documentMeta: { documentMeta: {
create: { create: {
subject: override?.subject || template.templateMeta?.subject, subject: override?.subject || template.templateMeta?.subject,

View File

@ -34,6 +34,7 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
templateMeta: true, templateMeta: true,
recipients: true, recipients: true,
fields: true, fields: true,
attachments: true,
user: { user: {
select: { select: {
id: true, id: true,

View File

@ -1,4 +1,4 @@
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client'; import type { Attachment, DocumentVisibility, Template, TemplateMeta } from '@prisma/client';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -21,6 +21,7 @@ export type UpdateTemplateOptions = {
publicDescription?: string; publicDescription?: string;
type?: Template['type']; type?: Template['type'];
useLegacyFieldInsertion?: boolean; useLegacyFieldInsertion?: boolean;
attachments?: Pick<Attachment, 'id' | 'label' | 'url'>[];
}; };
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>; meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
}; };
@ -53,6 +54,7 @@ export const updateTemplate = async ({
}, },
include: { include: {
templateMeta: true, templateMeta: true,
attachments: true,
}, },
}); });
@ -104,6 +106,29 @@ export const updateTemplate = async ({
publicDescription: data?.publicDescription, publicDescription: data?.publicDescription,
publicTitle: data?.publicTitle, publicTitle: data?.publicTitle,
useLegacyFieldInsertion: data?.useLegacyFieldInsertion, useLegacyFieldInsertion: data?.useLegacyFieldInsertion,
attachments: {
deleteMany: {
templateId,
id: {
notIn: data?.attachments?.map((attachment) => attachment.id),
},
},
upsert: data?.attachments?.map((attachment) => ({
where: {
id: attachment.id,
templateId,
},
update: {
label: attachment.label,
url: attachment.url,
},
create: {
id: attachment.id,
label: attachment.label,
url: attachment.url,
},
})),
},
authOptions, authOptions,
templateMeta: { templateMeta: {
upsert: { upsert: {

View File

@ -1,5 +1,6 @@
import type { z } from 'zod'; import type { z } from 'zod';
import { AttachmentSchema } from '@documenso/prisma/generated/zod/modelSchema/AttachmentSchema';
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
@ -62,6 +63,14 @@ export const ZTemplateSchema = TemplateSchema.pick({
}), }),
recipients: ZRecipientLiteSchema.array(), recipients: ZRecipientLiteSchema.array(),
fields: ZFieldSchema.array(), fields: ZFieldSchema.array(),
attachments: AttachmentSchema.pick({
id: true,
label: true,
url: true,
type: true,
})
.array()
.optional(),
}); });
export type TTemplate = z.infer<typeof ZTemplateSchema>; export type TTemplate = z.infer<typeof ZTemplateSchema>;

View File

@ -312,6 +312,8 @@ enum DocumentVisibility {
ADMIN ADMIN
} }
// Only "LINK" is supported for now.
// All other attachment types are not yet supported.
enum AttachmentType { enum AttachmentType {
FILE FILE
VIDEO VIDEO

View File

@ -14,6 +14,7 @@ import {
ZTemplateManySchema, ZTemplateManySchema,
ZTemplateSchema, ZTemplateSchema,
} from '@documenso/lib/types/template'; } from '@documenso/lib/types/template';
import AttachmentSchema from '@documenso/prisma/generated/zod/modelSchema/AttachmentSchema';
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
import { import {
@ -154,6 +155,16 @@ export const ZUpdateTemplateRequestSchema = z.object({
.optional(), .optional(),
type: z.nativeEnum(TemplateType).optional(), type: z.nativeEnum(TemplateType).optional(),
useLegacyFieldInsertion: z.boolean().optional(), useLegacyFieldInsertion: z.boolean().optional(),
attachments: AttachmentSchema.pick({
id: true,
label: true,
url: true,
})
.extend({
formId: z.string().min(1),
})
.array()
.optional(),
}) })
.optional(), .optional(),
meta: z meta: z

View File

@ -124,6 +124,11 @@ export const AddTemplateSettingsFormPartial = ({
emailSettings: ZDocumentEmailSettingsSchema.parse(template?.templateMeta?.emailSettings), emailSettings: ZDocumentEmailSettingsSchema.parse(template?.templateMeta?.emailSettings),
signatureTypes: extractTeamSignatureSettings(template?.templateMeta), signatureTypes: extractTeamSignatureSettings(template?.templateMeta),
}, },
attachments: template.attachments?.map((attachment) => ({
...attachment,
id: String(attachment.id),
formId: String(attachment.id),
})),
}, },
}); });
@ -138,12 +143,20 @@ export const AddTemplateSettingsFormPartial = ({
const onAddAttachment = () => { const onAddAttachment = () => {
appendAttachment({ appendAttachment({
id: nanoid(12),
formId: nanoid(12), formId: nanoid(12),
label: '', label: '',
link: '', url: '',
}); });
}; };
const onRemoveAttachment = (index: number) => {
removeAttachment(index);
const updatedAttachments = attachments.filter((_, idx) => idx !== index);
form.setValue('attachments', updatedAttachments);
};
const { stepIndex, currentStep, totalSteps, previousStep } = useStep(); const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
const distributionMethod = form.watch('meta.distributionMethod'); const distributionMethod = form.watch('meta.distributionMethod');
@ -622,7 +635,7 @@ export const AddTemplateSettingsFormPartial = ({
<div className="flex-1"> <div className="flex-1">
<FormField <FormField
control={form.control} control={form.control}
name={`attachments.${index}.link`} name={`attachments.${index}.url`}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex flex-row items-center"> <FormLabel className="flex flex-row items-center">
@ -641,7 +654,7 @@ export const AddTemplateSettingsFormPartial = ({
<div className="flex-none pt-8"> <div className="flex-none pt-8">
<button <button
onClick={() => removeAttachment(index)} onClick={() => onRemoveAttachment(index)}
className="hover:bg-muted rounded-md" className="hover:bg-muted rounded-md"
> >
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />

View File

@ -13,6 +13,7 @@ import {
} from '@documenso/lib/types/document-auth'; } from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url'; import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { AttachmentSchema } from '@documenso/prisma/generated/zod/modelSchema/AttachmentSchema';
import { import {
ZDocumentMetaDateFormatSchema, ZDocumentMetaDateFormatSchema,
ZDocumentMetaTimezoneSchema, ZDocumentMetaTimezoneSchema,
@ -55,13 +56,16 @@ export const ZAddTemplateSettingsFormSchema = z.object({
message: msg`At least one signature type must be enabled`.id, message: msg`At least one signature type must be enabled`.id,
}), }),
}), }),
attachments: z.array( attachments: AttachmentSchema.pick({
z.object({ id: true,
label: true,
url: true,
})
.extend({
formId: z.string().min(1), formId: z.string().min(1),
label: z.string(), })
link: z.string(), .array()
}), .optional(),
),
}); });
export type TAddTemplateSettingsFormSchema = z.infer<typeof ZAddTemplateSettingsFormSchema>; export type TAddTemplateSettingsFormSchema = z.infer<typeof ZAddTemplateSettingsFormSchema>;