feat: enhance document attachment handling and audit logging

- Added support for attachment updates in the updateDocument functionc.
- Introduced new audit log type for document attachments updates.
- Updated ZDocumentAuditLog schemas to include attachment-related events.
- Modified AddSettingsFormPartial to handle attachment IDs and types correctly.
- Set default value for attachment type in the Prisma schema.
This commit is contained in:
Catalin Documenso
2025-04-30 15:53:58 +03:00
parent e3f8e76e6a
commit 6980db57d3
8 changed files with 110 additions and 15 deletions

View File

@ -69,6 +69,7 @@ export const updateDocument = async ({
},
},
},
attachments: true,
},
});
@ -160,6 +161,8 @@ export const updateDocument = async ({
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
const isDocumentVisibilitySame =
data.visibility === undefined || data.visibility === document.visibility;
const isAttachmentsSame =
data.attachments === undefined || data.attachments === document.attachments;
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
@ -239,6 +242,20 @@ export const updateDocument = async ({
);
}
if (data.attachments) {
auditLogs.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ATTACHMENTS_UPDATED,
documentId,
metadata: requestMetadata,
data: {
from: document.attachments,
to: data.attachments,
},
}),
);
}
// Early return if nothing is required.
if (auditLogs.length === 0 && data.useLegacyFieldInsertion === undefined) {
return document;
@ -260,18 +277,42 @@ export const updateDocument = async ({
visibility: data.visibility as DocumentVisibility,
useLegacyFieldInsertion: data.useLegacyFieldInsertion,
authOptions,
attachments: {
deleteMany: {},
create:
data.attachments?.map((attachment) => ({
type: 'LINK',
label: attachment.label,
url: attachment.url,
})) || [],
},
});
if (data.attachments) {
await tx.attachment.deleteMany({
where: {
documentId,
id: {
notIn: data.attachments.map((a) => a.id),
},
},
});
await Promise.all(
data.attachments.map(
async (attachment) =>
await tx.attachment.upsert({
where: {
id: attachment.id,
documentId,
},
update: {
label: attachment.label,
url: attachment.url,
},
create: {
id: attachment.id,
label: attachment.label,
url: attachment.url,
documentId,
},
}),
),
);
}
await tx.documentAuditLog.createMany({
data: auditLogs,
});

View File

@ -39,6 +39,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
'DOCUMENT_ATTACHMENTS_UPDATED', // When the document attachments are updated.
]);
export const ZDocumentAuditLogEmailTypeSchema = z.enum([
@ -551,6 +552,29 @@ export const ZDocumentAuditLogEventDocumentMovedToTeamSchema = z.object({
}),
});
/**
* Event: Document attachments updated.
*/
export const ZDocumentAuditLogEventDocumentAttachmentsUpdatedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ATTACHMENTS_UPDATED),
data: z.object({
from: z.array(
z.object({
id: z.string(),
label: z.string(),
url: z.string(),
}),
),
to: z.array(
z.object({
id: z.string(),
label: z.string(),
url: z.string(),
}),
),
}),
});
export const ZDocumentAuditLogBaseSchema = z.object({
id: z.string(),
createdAt: z.date(),
@ -582,6 +606,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentSentSchema,
ZDocumentAuditLogEventDocumentTitleUpdatedSchema,
ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema,
ZDocumentAuditLogEventDocumentAttachmentsUpdatedSchema,
ZDocumentAuditLogEventFieldCreatedSchema,
ZDocumentAuditLogEventFieldRemovedSchema,
ZDocumentAuditLogEventFieldUpdatedSchema,

View File

@ -62,7 +62,6 @@ export const ZDocumentSchema = DocumentSchema.pick({
fields: ZFieldSchema.array(),
attachments: AttachmentSchema.pick({
id: true,
type: true,
label: true,
url: true,
})

View File

@ -388,6 +388,10 @@ export const formatDocumentAuditLogAction = (
anonymous: msg`Document completed`,
identified: msg`Document completed`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ATTACHMENTS_UPDATED }, ({ data }) => ({
anonymous: msg`Document attachments updated`,
identified: msg`${prefix} updated the document attachments ${data.to.map((a) => a.label).join(', ')}`,
}))
.exhaustive();
return {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Attachment" ALTER COLUMN "type" SET DEFAULT 'LINK';

View File

@ -323,7 +323,7 @@ enum AttachmentType {
model Attachment {
id String @id @default(uuid())
type AttachmentType
type AttachmentType @default(LINK)
label String
url String
createdAt DateTime @default(now())

View File

@ -97,9 +97,11 @@ export const AddSettingsFormPartial = ({
const defaultAttachments = [
{
id: '',
formId: initialId,
label: '',
url: '',
type: 'LINK',
},
];
@ -123,7 +125,12 @@ export const AddSettingsFormPartial = ({
language: document.documentMeta?.language ?? 'en',
signatureTypes: extractTeamSignatureSettings(document.documentMeta),
},
attachments: document.attachments ?? defaultAttachments,
attachments:
document.attachments?.map((attachment) => ({
...attachment,
id: String(attachment.id),
formId: String(attachment.id),
})) ?? defaultAttachments,
},
});
@ -136,6 +143,22 @@ export const AddSettingsFormPartial = ({
name: 'attachments',
});
const onRemoveAttachment = (index: number) => {
const attachment = attachments[index];
const formStateIndex =
form.getValues('attachments')?.findIndex((a) => a.formId === attachment.formId) ?? -1;
if (formStateIndex !== -1) {
removeAttachment(formStateIndex);
const updatedAttachments =
form.getValues('attachments')?.filter((a) => a.formId !== attachment.formId) ?? [];
form.setValue('attachments', updatedAttachments);
}
};
const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
const documentHasBeenSent = recipients.some(
@ -144,11 +167,11 @@ export const AddSettingsFormPartial = ({
const onAddAttachment = () => {
appendAttachment({
id: nanoid(12),
formId: nanoid(12),
label: '',
url: '',
// fix this
id: '',
type: 'LINK',
});
};
@ -533,7 +556,7 @@ export const AddSettingsFormPartial = ({
<div className="flex-none pt-8">
<button
onClick={() => removeAttachment(index)}
onClick={() => onRemoveAttachment(index)}
className="hover:bg-muted rounded-md"
>
<Trash className="h-4 w-4" />

View File

@ -63,6 +63,7 @@ export const ZAddSettingsFormSchema = z.object({
id: true,
label: true,
url: true,
type: true,
})
.extend({
formId: z.string().min(1),