From dd5ae664497e9c29292d904735cf6767ba2d390a Mon Sep 17 00:00:00 2001 From: Naifer <161821705+omarNaifer12@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:36:02 +0100 Subject: [PATCH] Fix: deletion and name updates of attachments in body reflected in Files (#13169) resolve #13168 , #8501 This PR fixes an issue where the onChange trigger was deleting all attachments by removing the JWT token from their paths (if it existed) and comparing the new body with the old body to identify deleted attachments. It ensures that only attachments actually removed from the body get deleted, preventing unintended deletion of attachments not added directly through the body. It also handles updating attachment names in the body so changes are reflected in Files, with related tests updated accordingly. https://github.com/user-attachments/assets/8d824a24-b257-4794-942e-3b2dceb9907d --------- Co-authored-by: Lucas Bordeau --- .../components/ActivityRichTextEditor.tsx | 31 ++++++-- .../utils/__tests__/compareUrls.test.ts | 33 +++++++++ .../filterAttachmentsToRestore.test.ts | 10 +-- ...tivityAttachmentIdsAndNameToUpdate.test.ts | 74 +++++++++++++++++++ .../getActivityAttachmentIdsToDelete.test.ts | 54 +++++++++++--- .../getActivityAttachmentPaths.test.ts | 40 ---------- .../getActivityAttachmentPathsAndName.test.ts | 57 ++++++++++++++ ...etActivityAttachmentPathsToRestore.test.ts | 14 ++-- .../utils/__tests__/getAttachmentPath.test.ts | 14 +++- .../modules/activities/utils/compareUrls.ts | 14 ++++ .../utils/filterAttachmentsToRestore.ts | 6 +- ...getActivityAttachmentIdsAndNameToUpdate.ts | 29 ++++++++ .../utils/getActivityAttachmentIdsToDelete.ts | 25 ++++++- ...s => getActivityAttachmentPathsAndName.ts} | 16 ++-- .../getActivityAttachmentPathsToRestore.ts | 21 ++++-- .../activities/utils/getAttachmentPath.ts | 27 ++++++- 16 files changed, 373 insertions(+), 92 deletions(-) create mode 100644 packages/twenty-front/src/modules/activities/utils/__tests__/compareUrls.test.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsAndNameToUpdate.test.ts delete mode 100644 packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPaths.test.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsAndName.test.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/compareUrls.ts create mode 100644 packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsAndNameToUpdate.ts rename packages/twenty-front/src/modules/activities/utils/{getActivityAttachmentPaths.ts => getActivityAttachmentPathsAndName.ts} (51%) diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index abbd27fac..118f71616 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -20,6 +20,7 @@ import { Attachment } from '@/activities/files/types/Attachment'; import { Note } from '@/activities/types/Note'; import { Task } from '@/activities/types/Task'; import { filterAttachmentsToRestore } from '@/activities/utils/filterAttachmentsToRestore'; +import { getActivityAttachmentIdsAndNameToUpdate } from '@/activities/utils/getActivityAttachmentIdsAndNameToUpdate'; import { getActivityAttachmentIdsToDelete } from '@/activities/utils/getActivityAttachmentIdsToDelete'; import { getActivityAttachmentPathsToRestore } from '@/activities/utils/getActivityAttachmentPathsToRestore'; import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId'; @@ -27,6 +28,7 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly'; import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState'; import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; @@ -51,10 +53,6 @@ type ActivityRichTextEditorProps = { | CoreObjectNameSingular.Note; }; -type Activity = (Task | Note) & { - attachments: Attachment[]; -}; - export const ActivityRichTextEditor = ({ activityId, activityObjectNameSingular, @@ -101,7 +99,9 @@ export const ActivityRichTextEditor = ({ }, }, }); - + const { updateOneRecord: updateOneAttachment } = useUpdateOneRecord({ + objectNameSingular: CoreObjectNameSingular.Attachment, + }); const { upsertActivity } = useUpsertActivity({ activityObjectNameSingular: activityObjectNameSingular, }); @@ -177,7 +177,7 @@ export const ActivityRichTextEditor = ({ async (newStringifiedBody: string) => { const oldActivity = snapshot .getLoadable(recordStoreFamilyState(activityId)) - .getValue() as Activity; + .getValue(); set(recordStoreFamilyState(activityId), (oldActivity) => { return { @@ -209,7 +209,8 @@ export const ActivityRichTextEditor = ({ const attachmentIdsToDelete = getActivityAttachmentIdsToDelete( newStringifiedBody, - oldActivity.attachments, + oldActivity?.attachments, + oldActivity?.bodyV2.blocknote, ); if (attachmentIdsToDelete.length > 0) { @@ -220,7 +221,7 @@ export const ActivityRichTextEditor = ({ const attachmentPathsToRestore = getActivityAttachmentPathsToRestore( newStringifiedBody, - oldActivity.attachments, + oldActivity?.attachments, ); if (attachmentPathsToRestore.length > 0) { @@ -236,6 +237,19 @@ export const ActivityRichTextEditor = ({ idsToRestore: attachmentIdsToRestore, }); } + const attachmentsToUpdate = getActivityAttachmentIdsAndNameToUpdate( + newStringifiedBody, + oldActivity?.attachments, + ); + if (attachmentsToUpdate.length > 0) { + for (const attachmentToUpdate of attachmentsToUpdate) { + if (!attachmentToUpdate.id) continue; + await updateOneAttachment({ + idToUpdate: attachmentToUpdate.id, + updateOneRecordInput: { name: attachmentToUpdate.name }, + }); + } + } }, [ activityId, @@ -245,6 +259,7 @@ export const ActivityRichTextEditor = ({ deleteAttachments, restoreAttachments, findSoftDeletedAttachments, + updateOneAttachment, ], ); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/compareUrls.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/compareUrls.test.ts new file mode 100644 index 000000000..a01d73f42 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/compareUrls.test.ts @@ -0,0 +1,33 @@ +import { compareUrls } from '@/activities/utils/compareUrls'; + +describe('compareUrls', () => { + it('should return true if the two URLs are the same except for the token segment', () => { + const firstToken = 'ugudgugugxsqv'; + const secondToken = 'yugguzvdhvyuzede'; + const urlA = `https://exemple.com/files/attachment/${firstToken}/test.txt`; + const urlB = `https://exemple.com/files/attachment/${secondToken}/test.txt`; + const result = compareUrls(urlA, urlB); + expect(result).toEqual(true); + }); + + it('should return true if the two URLs are exactly the same', () => { + const urlA = `https://exemple.com/files/images/test.txt`; + const urlB = `https://exemple.com/files/images/test.txt`; + const result = compareUrls(urlA, urlB); + expect(result).toEqual(true); + }); + + it('should return false if filenames are different in the same domain', () => { + const urlA = `https://exemple.com/files/images/test1.txt`; + const urlB = `https://exemple.com/files/images/test.txt`; + const result = compareUrls(urlA, urlB); + expect(result).toEqual(false); + }); + + it('should return false if the domains are different', () => { + const urlA = `https://exemple1.com/files/images/test1.txt`; + const urlB = `https://exemple.com/files/images/test.txt`; + const result = compareUrls(urlA, urlB); + expect(result).toEqual(false); + }); +}); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts index 9b4b07f2f..e68a94b6a 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts @@ -6,7 +6,7 @@ describe('filterAttachmentsToRestore', () => { const softDeletedAttachments = [ { id: '1', - fullPath: 'test.txt', + fullPath: 'https://exemple.com/test.txt', }, ] as Attachment[]; const attachmentIdsToRestore = filterAttachmentsToRestore( @@ -18,7 +18,7 @@ describe('filterAttachmentsToRestore', () => { it('should not return any ids if there are no soft deleted attachments', () => { const attachmentIdsToRestore = filterAttachmentsToRestore( - ['/files/attachment/test.txt'], + ['https://exemple.com/files/attachment/test.txt'], [], ); expect(attachmentIdsToRestore).toEqual([]); @@ -28,15 +28,15 @@ describe('filterAttachmentsToRestore', () => { const softDeletedAttachments = [ { id: '1', - fullPath: '/files/attachment/test.txt', + fullPath: 'https://exemple.com/files/images/test.txt', }, { id: '2', - fullPath: '/files/attachment/test2.txt', + fullPath: 'https://exemple.com/files/images/test2.txt', }, ] as Attachment[]; const attachmentIdsToRestore = filterAttachmentsToRestore( - ['attachment/test.txt'], + ['https://exemple.com/files/images/test.txt'], softDeletedAttachments, ); expect(attachmentIdsToRestore).toEqual(['1']); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsAndNameToUpdate.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsAndNameToUpdate.test.ts new file mode 100644 index 000000000..aac9df9c0 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsAndNameToUpdate.test.ts @@ -0,0 +1,74 @@ +import { Attachment } from '@/activities/files/types/Attachment'; +import { getActivityAttachmentIdsAndNameToUpdate } from '@/activities/utils/getActivityAttachmentIdsAndNameToUpdate'; + +describe('getActivityAttachmentIdsAndNameToUpdate', () => { + it('should return an empty array when attachment names are not changed in the body', () => { + const attachments = [ + { + id: '1', + fullPath: 'https://exemple.com/files/images/test.txt', + name: 'image', + }, + { + id: '2', + fullPath: 'https://exemple.com/files/images/test2.txt', + name: 'image1', + }, + ] as Attachment[]; + + const activityBody = JSON.stringify([ + { + type: 'file', + props: { + url: 'https://exemple.com/files/images/test.txt', + name: 'image', + }, + }, + { + type: 'file', + props: { + url: 'https://exemple.com/files/images/test2.txt', + name: 'image1', + }, + }, + ]); + const attachmentIdsAndNameToUpdate = + getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments); + expect(attachmentIdsAndNameToUpdate).toEqual([]); + }); + + it('should return only the IDs and new names of attachments whose names were changed in the body', () => { + const attachments = [ + { + id: '1', + fullPath: 'https://exemple.com/files/images/test.txt', + name: 'image', + }, + { + id: '2', + fullPath: 'https://exemple.com/files/images/test2.txt', + name: 'image1', + }, + ] as Attachment[]; + + const activityBody = JSON.stringify([ + { + type: 'file', + props: { + url: 'https://exemple.com/files/images/test.txt', + name: 'image', + }, + }, + { + type: 'file', + props: { + url: 'https://exemple.com/files/images/test2.txt', + name: 'image4', + }, + }, + ]); + const attachmentIdsAndNameToUpdate = + getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments); + expect(attachmentIdsAndNameToUpdate).toEqual([{ id: '2', name: 'image4' }]); + }); +}); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts index 853241137..1852e0603 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts @@ -6,17 +6,37 @@ describe('getActivityAttachmentIdsToDelete', () => { const attachments = [ { id: '1', - fullPath: '/files/attachment/test.txt', + fullPath: 'https://example.com/files/images/test.txt', }, { id: '2', - fullPath: '/files/attachment/test2.txt', + fullPath: 'https://example.com/files/images/test2.txt', }, ] as Attachment[]; - + const newActivityBody = JSON.stringify([ + { + type: 'file', + props: { url: 'https://example.com/files/images/test.txt' }, + }, + { + type: 'file', + props: { url: 'https://example.com/files/images/test2.txt' }, + }, + ]); + const oldActivityBody = JSON.stringify([ + { + type: 'file', + props: { url: 'https://example.com/files/images/test.txt' }, + }, + { + type: 'file', + props: { url: 'https://example.com/files/images/test2.txt' }, + }, + ]); const attachmentIdsToDelete = getActivityAttachmentIdsToDelete( - '/files/attachment/test2.txt /files/attachment/test.txt', + newActivityBody, attachments, + oldActivityBody, ); expect(attachmentIdsToDelete).toEqual([]); }); @@ -25,18 +45,34 @@ describe('getActivityAttachmentIdsToDelete', () => { const attachments = [ { id: '1', - fullPath: '/files/attachment/test.txt', + fullPath: 'https://example.com/files/images/test.txt', }, { id: '2', - fullPath: '/files/attachment/test2.txt', + fullPath: 'https://example.com/files/images/test2.txt', }, ] as Attachment[]; - + const newActivityBody = JSON.stringify([ + { + type: 'file', + props: { url: 'https://example.com/files/images/test.txt' }, + }, + ]); + const oldActivityBody = JSON.stringify([ + { + type: 'file', + props: { url: 'https://example.com/files/images/test.txt' }, + }, + { + type: 'file', + props: { url: 'https://example.com/files/images/test2.txt' }, + }, + ]); const attachmentIdsToDelete = getActivityAttachmentIdsToDelete( - '/files/attachment/test2.txt', + newActivityBody, attachments, + oldActivityBody, ); - expect(attachmentIdsToDelete).toEqual(['1']); + expect(attachmentIdsToDelete).toEqual(['2']); }); }); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPaths.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPaths.test.ts deleted file mode 100644 index 0a32150ee..000000000 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPaths.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getActivityAttachmentPaths } from '@/activities/utils/getActivityAttachmentPaths'; - -describe('getActivityAttachmentPaths', () => { - it('should return the file paths from the activity blocknote', () => { - const activityBlocknote = JSON.stringify([ - { type: 'paragraph', props: { text: 'test' } }, - { - type: 'image', - props: { - url: 'https://example.com/files/attachment/image.jpg?queryParam=value', - }, - }, - { - type: 'file', - props: { - url: 'https://example.com/files/attachment/file.pdf?queryParam=value', - }, - }, - { - type: 'video', - props: { - url: 'https://example.com/files/attachment/video.mp4?queryParam=value', - }, - }, - { - type: 'audio', - props: { - url: 'https://example.com/files/attachment/audio.mp3?queryParam=value', - }, - }, - ]); - const res = getActivityAttachmentPaths(activityBlocknote); - expect(res).toEqual([ - 'attachment/image.jpg', - 'attachment/file.pdf', - 'attachment/video.mp4', - 'attachment/audio.mp3', - ]); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsAndName.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsAndName.test.ts new file mode 100644 index 000000000..724b1c3b5 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsAndName.test.ts @@ -0,0 +1,57 @@ +import { getActivityAttachmentPathsAndName } from '@/activities/utils/getActivityAttachmentPathsAndName'; + +describe('getActivityAttachmentPathsAndName', () => { + it('should return the file paths and names from the activity blocknote', () => { + const activityBlocknote = JSON.stringify([ + { type: 'paragraph', props: { text: 'test' } }, + { + type: 'image', + props: { + url: 'https://example.com/files/image/image.jpg?queryParam=value', + name: 'image', + }, + }, + { + type: 'file', + props: { + url: 'https://example.com/files/file/file.pdf?queryParam=value', + name: 'file', + }, + }, + { + type: 'video', + props: { + url: 'https://example.com/files/video/video.mp4?queryParam=value', + name: 'video', + }, + }, + { + type: 'audio', + props: { + url: 'https://example.com/files/audio/audio.mp3?queryParam=value', + name: 'audio', + }, + }, + ]); + const res = getActivityAttachmentPathsAndName(activityBlocknote); + + expect(res).toEqual([ + { + path: 'https://example.com/files/image/image.jpg?queryParam=value', + name: 'image', + }, + { + path: 'https://example.com/files/file/file.pdf?queryParam=value', + name: 'file', + }, + { + path: 'https://example.com/files/video/video.mp4?queryParam=value', + name: 'video', + }, + { + path: 'https://example.com/files/audio/audio.mp3?queryParam=value', + name: 'audio', + }, + ]); + }); +}); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsToRestore.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsToRestore.test.ts index 717e5322e..53dc2689f 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsToRestore.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsToRestore.test.ts @@ -8,14 +8,12 @@ describe('getActivityAttachmentPathsToRestore', () => { type: 'paragraph', }, ]); - const oldActivityAttachments = [ { id: '1', - fullPath: '/files/attachment/test.txt', + fullPath: 'https://example.com/files/images/test.txt', }, ] as Attachment[]; - const attachmentPathsToRestore = getActivityAttachmentPathsToRestore( newActivityBody, oldActivityAttachments, @@ -27,18 +25,18 @@ describe('getActivityAttachmentPathsToRestore', () => { const newActivityBody = JSON.stringify([ { type: 'file', - props: { url: '/files/attachment/test.txt' }, + props: { url: 'https://example.com/files/images/test.txt' }, }, { type: 'file', - props: { url: '/files/attachment/test2.txt' }, + props: { url: 'https://example.com/files/images/test2.txt' }, }, ]); const oldActivityAttachments = [ { id: '1', - fullPath: '/files/attachment/test.txt', + fullPath: 'https://example.com/files/images/test.txt', }, ] as Attachment[]; @@ -46,6 +44,8 @@ describe('getActivityAttachmentPathsToRestore', () => { newActivityBody, oldActivityAttachments, ); - expect(attachmentPathsToRestore).toEqual(['attachment/test2.txt']); + expect(attachmentPathsToRestore).toEqual([ + 'https://example.com/files/images/test2.txt', + ]); }); }); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getAttachmentPath.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getAttachmentPath.test.ts index 6047b442c..80c19488f 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getAttachmentPath.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getAttachmentPath.test.ts @@ -1,10 +1,18 @@ import { getAttachmentPath } from '@/activities/utils/getAttachmentPath'; describe('getAttachmentPath', () => { - it('should return the attachment path', () => { + it('should extract the correct path from a locally stored file URL with token', () => { + const token = 'dybszrrxvgrtefeidgybxzfxzr'; const res = getAttachmentPath( - 'https://example.com/files/attachment/image.jpg?queryParam=value', + `https://server.com/files/attachment/${token}/image.jpg?queryParam=value`, ); - expect(res).toEqual('attachment/image.jpg'); + expect(res).toEqual('https://server.com/files/attachment/image.jpg'); + }); + + it('should extract the correct path from a regular file URL', () => { + const res = getAttachmentPath( + 'https://exemple.com/files/images/image.jpg?queryParam=value', + ); + expect(res).toEqual('https://exemple.com/files/images/image.jpg'); }); }); diff --git a/packages/twenty-front/src/modules/activities/utils/compareUrls.ts b/packages/twenty-front/src/modules/activities/utils/compareUrls.ts new file mode 100644 index 000000000..8d745d971 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/compareUrls.ts @@ -0,0 +1,14 @@ +import { getAttachmentPath } from '@/activities/utils/getAttachmentPath'; + +export const compareUrls = ( + firstAttachmentUrl: string, + secondAttachmentUrl: string, +): boolean => { + const urlA = new URL(firstAttachmentUrl); + const urlB = new URL(secondAttachmentUrl); + if (urlA.hostname !== urlB.hostname) return false; + return ( + getAttachmentPath(firstAttachmentUrl) === + getAttachmentPath(secondAttachmentUrl) + ); +}; diff --git a/packages/twenty-front/src/modules/activities/utils/filterAttachmentsToRestore.ts b/packages/twenty-front/src/modules/activities/utils/filterAttachmentsToRestore.ts index cc8e2ce30..29b39a6fc 100644 --- a/packages/twenty-front/src/modules/activities/utils/filterAttachmentsToRestore.ts +++ b/packages/twenty-front/src/modules/activities/utils/filterAttachmentsToRestore.ts @@ -1,5 +1,5 @@ import { Attachment } from '@/activities/files/types/Attachment'; -import { getAttachmentPath } from '@/activities/utils/getAttachmentPath'; +import { compareUrls } from '@/activities/utils/compareUrls'; export const filterAttachmentsToRestore = ( attachmentPathsToRestore: string[], @@ -7,8 +7,8 @@ export const filterAttachmentsToRestore = ( ) => { return softDeletedAttachments .filter((attachment) => - attachmentPathsToRestore.some( - (path) => getAttachmentPath(attachment.fullPath) === path, + attachmentPathsToRestore.some((path) => + compareUrls(attachment.fullPath, path), ), ) .map((attachment) => attachment.id); diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsAndNameToUpdate.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsAndNameToUpdate.ts new file mode 100644 index 000000000..5aeb29370 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsAndNameToUpdate.ts @@ -0,0 +1,29 @@ +import { Attachment } from '@/activities/files/types/Attachment'; +import { compareUrls } from '@/activities/utils/compareUrls'; +import { + AttachmentInfo, + getActivityAttachmentPathsAndName, +} from '@/activities/utils/getActivityAttachmentPathsAndName'; +import { isDefined } from 'twenty-shared/utils'; + +export const getActivityAttachmentIdsAndNameToUpdate = ( + newActivityBody: string, + oldActivityAttachments: Attachment[] = [], +) => { + const activityAttachmentsNameAndPaths = + getActivityAttachmentPathsAndName(newActivityBody); + if (activityAttachmentsNameAndPaths.length === 0) return []; + + return activityAttachmentsNameAndPaths.reduce( + (acc: Partial[], activity: AttachmentInfo) => { + const foundActivity = oldActivityAttachments.find((attachment) => + compareUrls(attachment.fullPath, activity.path), + ); + if (isDefined(foundActivity) && foundActivity.name !== activity.name) { + acc.push({ id: foundActivity.id, name: activity.name }); + } + return acc; + }, + [], + ); +}; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts index 2709d7288..828b413b6 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts @@ -1,15 +1,34 @@ import { Attachment } from '@/activities/files/types/Attachment'; +import { compareUrls } from '@/activities/utils/compareUrls'; +import { getActivityAttachmentPathsAndName } from '@/activities/utils/getActivityAttachmentPathsAndName'; export const getActivityAttachmentIdsToDelete = ( newActivityBody: string, oldActivityAttachments: Attachment[] = [], + oldActivityBody: string, ) => { if (oldActivityAttachments.length === 0) return []; - return oldActivityAttachments + const newActivityAttachmentPaths = + getActivityAttachmentPathsAndName(newActivityBody); + + const oldActivityAttachmentPaths = + getActivityAttachmentPathsAndName(oldActivityBody); + + const pathsToDelete = oldActivityAttachmentPaths .filter( - (attachment) => - !newActivityBody.includes(attachment.fullPath.split('?')[0]), + (oldActivity) => + !newActivityAttachmentPaths.some( + (newActivity) => newActivity.path === oldActivity.path, + ), + ) + .map((activity) => activity.path); + + return oldActivityAttachments + .filter((attachment) => + pathsToDelete.some((pathToDelete) => + compareUrls(attachment.fullPath, pathToDelete), + ), ) .map((attachment) => attachment.id); }; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPaths.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsAndName.ts similarity index 51% rename from packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPaths.ts rename to packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsAndName.ts index 5af59a735..6919b7ff7 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPaths.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsAndName.ts @@ -1,17 +1,23 @@ -import { getAttachmentPath } from '@/activities/utils/getAttachmentPath'; import { isNonEmptyString } from '@sniptt/guards'; -export const getActivityAttachmentPaths = ( +export type AttachmentInfo = { + path: string; + name: string; +}; +export const getActivityAttachmentPathsAndName = ( stringifiedActivityBlocknote: string, -): string[] => { +): AttachmentInfo[] => { const activityBlocknote = JSON.parse(stringifiedActivityBlocknote ?? '{}'); - return activityBlocknote.reduce((acc: string[], block: any) => { + return activityBlocknote.reduce((acc: AttachmentInfo[], block: any) => { if ( ['image', 'file', 'video', 'audio'].includes(block.type) && isNonEmptyString(block.props.url) ) { - acc.push(getAttachmentPath(block.props.url)); + acc.push({ + path: block.props.url, + name: block.props.name, + }); } return acc; }, []); diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsToRestore.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsToRestore.ts index 365ad8e1c..1ce53cc39 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsToRestore.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsToRestore.ts @@ -1,17 +1,22 @@ import { Attachment } from '@/activities/files/types/Attachment'; -import { getActivityAttachmentPaths } from '@/activities/utils/getActivityAttachmentPaths'; -import { getAttachmentPath } from '@/activities/utils/getAttachmentPath'; +import { compareUrls } from '@/activities/utils/compareUrls'; +import { getActivityAttachmentPathsAndName } from '@/activities/utils/getActivityAttachmentPathsAndName'; export const getActivityAttachmentPathsToRestore = ( newActivityBody: string, oldActivityAttachments: Attachment[], ) => { const newActivityAttachmentPaths = - getActivityAttachmentPaths(newActivityBody); + getActivityAttachmentPathsAndName(newActivityBody); - return newActivityAttachmentPaths.filter((fullPath) => - oldActivityAttachments.every( - (attachment) => getAttachmentPath(attachment.fullPath) !== fullPath, - ), - ); + const pathsToRestore = newActivityAttachmentPaths + .filter( + (newActivity) => + !oldActivityAttachments.some((attachment) => + compareUrls(newActivity.path, attachment.fullPath), + ), + ) + .map((activity) => activity.path); + + return pathsToRestore; }; diff --git a/packages/twenty-front/src/modules/activities/utils/getAttachmentPath.ts b/packages/twenty-front/src/modules/activities/utils/getAttachmentPath.ts index 8aaac26e2..5c18efcde 100644 --- a/packages/twenty-front/src/modules/activities/utils/getAttachmentPath.ts +++ b/packages/twenty-front/src/modules/activities/utils/getAttachmentPath.ts @@ -1,3 +1,28 @@ export const getAttachmentPath = (attachmentFullPath: string) => { - return attachmentFullPath.split('/files/')[1].split('?')[0]; + if (!attachmentFullPath.includes('/files/')) { + return attachmentFullPath?.split('?')[0]; + } + + const base = attachmentFullPath?.split('/files/')[0]; + const rawPath = attachmentFullPath?.split('/files/')[1]?.split('?')[0]; + + if (!rawPath) { + throw new Error(`Invalid attachment path: ${attachmentFullPath}`); + } + + if (!rawPath.startsWith('attachment/')) { + return attachmentFullPath?.split('?')[0]; + } + + const pathParts = rawPath.split('/'); + if (pathParts.length < 2) { + throw new Error( + `Invalid attachment path structure: ${rawPath}. Path must have at least two segments.`, + ); + } + const filename = pathParts.pop(); + + pathParts.pop(); + + return `${base}/files/${pathParts.join('/')}/${filename}`; };