Skip to content

Commit a024f12

Browse files
omarNaifer12lucasbordeau
authored andcommitted
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 <bordeau.lucas@gmail.com>
1 parent 278d7da commit a024f12

16 files changed

+373
-92
lines changed

packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import { Attachment } from '@/activities/files/types/Attachment';
2020
import { Note } from '@/activities/types/Note';
2121
import { Task } from '@/activities/types/Task';
2222
import { filterAttachmentsToRestore } from '@/activities/utils/filterAttachmentsToRestore';
23+
import { getActivityAttachmentIdsAndNameToUpdate } from '@/activities/utils/getActivityAttachmentIdsAndNameToUpdate';
2324
import { getActivityAttachmentIdsToDelete } from '@/activities/utils/getActivityAttachmentIdsToDelete';
2425
import { getActivityAttachmentPathsToRestore } from '@/activities/utils/getActivityAttachmentPathsToRestore';
2526
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
2627
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
2728
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
2829
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
2930
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
31+
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
3032
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
3133
import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState';
3234
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
@@ -51,10 +53,6 @@ type ActivityRichTextEditorProps = {
5153
| CoreObjectNameSingular.Note;
5254
};
5355

54-
type Activity = (Task | Note) & {
55-
attachments: Attachment[];
56-
};
57-
5856
export const ActivityRichTextEditor = ({
5957
activityId,
6058
activityObjectNameSingular,
@@ -101,7 +99,9 @@ export const ActivityRichTextEditor = ({
10199
},
102100
},
103101
});
104-
102+
const { updateOneRecord: updateOneAttachment } = useUpdateOneRecord({
103+
objectNameSingular: CoreObjectNameSingular.Attachment,
104+
});
105105
const { upsertActivity } = useUpsertActivity({
106106
activityObjectNameSingular: activityObjectNameSingular,
107107
});
@@ -177,7 +177,7 @@ export const ActivityRichTextEditor = ({
177177
async (newStringifiedBody: string) => {
178178
const oldActivity = snapshot
179179
.getLoadable(recordStoreFamilyState(activityId))
180-
.getValue() as Activity;
180+
.getValue();
181181

182182
set(recordStoreFamilyState(activityId), (oldActivity) => {
183183
return {
@@ -209,7 +209,8 @@ export const ActivityRichTextEditor = ({
209209

210210
const attachmentIdsToDelete = getActivityAttachmentIdsToDelete(
211211
newStringifiedBody,
212-
oldActivity.attachments,
212+
oldActivity?.attachments,
213+
oldActivity?.bodyV2.blocknote,
213214
);
214215

215216
if (attachmentIdsToDelete.length > 0) {
@@ -220,7 +221,7 @@ export const ActivityRichTextEditor = ({
220221

221222
const attachmentPathsToRestore = getActivityAttachmentPathsToRestore(
222223
newStringifiedBody,
223-
oldActivity.attachments,
224+
oldActivity?.attachments,
224225
);
225226

226227
if (attachmentPathsToRestore.length > 0) {
@@ -236,6 +237,19 @@ export const ActivityRichTextEditor = ({
236237
idsToRestore: attachmentIdsToRestore,
237238
});
238239
}
240+
const attachmentsToUpdate = getActivityAttachmentIdsAndNameToUpdate(
241+
newStringifiedBody,
242+
oldActivity?.attachments,
243+
);
244+
if (attachmentsToUpdate.length > 0) {
245+
for (const attachmentToUpdate of attachmentsToUpdate) {
246+
if (!attachmentToUpdate.id) continue;
247+
await updateOneAttachment({
248+
idToUpdate: attachmentToUpdate.id,
249+
updateOneRecordInput: { name: attachmentToUpdate.name },
250+
});
251+
}
252+
}
239253
},
240254
[
241255
activityId,
@@ -245,6 +259,7 @@ export const ActivityRichTextEditor = ({
245259
deleteAttachments,
246260
restoreAttachments,
247261
findSoftDeletedAttachments,
262+
updateOneAttachment,
248263
],
249264
);
250265

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { compareUrls } from '@/activities/utils/compareUrls';
2+
3+
describe('compareUrls', () => {
4+
it('should return true if the two URLs are the same except for the token segment', () => {
5+
const firstToken = 'ugudgugugxsqv';
6+
const secondToken = 'yugguzvdhvyuzede';
7+
const urlA = `https://exemple.com/files/attachment/${firstToken}/test.txt`;
8+
const urlB = `https://exemple.com/files/attachment/${secondToken}/test.txt`;
9+
const result = compareUrls(urlA, urlB);
10+
expect(result).toEqual(true);
11+
});
12+
13+
it('should return true if the two URLs are exactly the same', () => {
14+
const urlA = `https://exemple.com/files/images/test.txt`;
15+
const urlB = `https://exemple.com/files/images/test.txt`;
16+
const result = compareUrls(urlA, urlB);
17+
expect(result).toEqual(true);
18+
});
19+
20+
it('should return false if filenames are different in the same domain', () => {
21+
const urlA = `https://exemple.com/files/images/test1.txt`;
22+
const urlB = `https://exemple.com/files/images/test.txt`;
23+
const result = compareUrls(urlA, urlB);
24+
expect(result).toEqual(false);
25+
});
26+
27+
it('should return false if the domains are different', () => {
28+
const urlA = `https://exemple1.com/files/images/test1.txt`;
29+
const urlB = `https://exemple.com/files/images/test.txt`;
30+
const result = compareUrls(urlA, urlB);
31+
expect(result).toEqual(false);
32+
});
33+
});

packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('filterAttachmentsToRestore', () => {
66
const softDeletedAttachments = [
77
{
88
id: '1',
9-
fullPath: 'test.txt',
9+
fullPath: 'https://exemple.com/test.txt',
1010
},
1111
] as Attachment[];
1212
const attachmentIdsToRestore = filterAttachmentsToRestore(
@@ -18,7 +18,7 @@ describe('filterAttachmentsToRestore', () => {
1818

1919
it('should not return any ids if there are no soft deleted attachments', () => {
2020
const attachmentIdsToRestore = filterAttachmentsToRestore(
21-
['/files/attachment/test.txt'],
21+
['https://exemple.com/files/attachment/test.txt'],
2222
[],
2323
);
2424
expect(attachmentIdsToRestore).toEqual([]);
@@ -28,15 +28,15 @@ describe('filterAttachmentsToRestore', () => {
2828
const softDeletedAttachments = [
2929
{
3030
id: '1',
31-
fullPath: '/files/attachment/test.txt',
31+
fullPath: 'https://exemple.com/files/images/test.txt',
3232
},
3333
{
3434
id: '2',
35-
fullPath: '/files/attachment/test2.txt',
35+
fullPath: 'https://exemple.com/files/images/test2.txt',
3636
},
3737
] as Attachment[];
3838
const attachmentIdsToRestore = filterAttachmentsToRestore(
39-
['attachment/test.txt'],
39+
['https://exemple.com/files/images/test.txt'],
4040
softDeletedAttachments,
4141
);
4242
expect(attachmentIdsToRestore).toEqual(['1']);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Attachment } from '@/activities/files/types/Attachment';
2+
import { getActivityAttachmentIdsAndNameToUpdate } from '@/activities/utils/getActivityAttachmentIdsAndNameToUpdate';
3+
4+
describe('getActivityAttachmentIdsAndNameToUpdate', () => {
5+
it('should return an empty array when attachment names are not changed in the body', () => {
6+
const attachments = [
7+
{
8+
id: '1',
9+
fullPath: 'https://exemple.com/files/images/test.txt',
10+
name: 'image',
11+
},
12+
{
13+
id: '2',
14+
fullPath: 'https://exemple.com/files/images/test2.txt',
15+
name: 'image1',
16+
},
17+
] as Attachment[];
18+
19+
const activityBody = JSON.stringify([
20+
{
21+
type: 'file',
22+
props: {
23+
url: 'https://exemple.com/files/images/test.txt',
24+
name: 'image',
25+
},
26+
},
27+
{
28+
type: 'file',
29+
props: {
30+
url: 'https://exemple.com/files/images/test2.txt',
31+
name: 'image1',
32+
},
33+
},
34+
]);
35+
const attachmentIdsAndNameToUpdate =
36+
getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments);
37+
expect(attachmentIdsAndNameToUpdate).toEqual([]);
38+
});
39+
40+
it('should return only the IDs and new names of attachments whose names were changed in the body', () => {
41+
const attachments = [
42+
{
43+
id: '1',
44+
fullPath: 'https://exemple.com/files/images/test.txt',
45+
name: 'image',
46+
},
47+
{
48+
id: '2',
49+
fullPath: 'https://exemple.com/files/images/test2.txt',
50+
name: 'image1',
51+
},
52+
] as Attachment[];
53+
54+
const activityBody = JSON.stringify([
55+
{
56+
type: 'file',
57+
props: {
58+
url: 'https://exemple.com/files/images/test.txt',
59+
name: 'image',
60+
},
61+
},
62+
{
63+
type: 'file',
64+
props: {
65+
url: 'https://exemple.com/files/images/test2.txt',
66+
name: 'image4',
67+
},
68+
},
69+
]);
70+
const attachmentIdsAndNameToUpdate =
71+
getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments);
72+
expect(attachmentIdsAndNameToUpdate).toEqual([{ id: '2', name: 'image4' }]);
73+
});
74+
});

packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,37 @@ describe('getActivityAttachmentIdsToDelete', () => {
66
const attachments = [
77
{
88
id: '1',
9-
fullPath: '/files/attachment/test.txt',
9+
fullPath: 'https://example.com/files/images/test.txt',
1010
},
1111
{
1212
id: '2',
13-
fullPath: '/files/attachment/test2.txt',
13+
fullPath: 'https://example.com/files/images/test2.txt',
1414
},
1515
] as Attachment[];
16-
16+
const newActivityBody = JSON.stringify([
17+
{
18+
type: 'file',
19+
props: { url: 'https://example.com/files/images/test.txt' },
20+
},
21+
{
22+
type: 'file',
23+
props: { url: 'https://example.com/files/images/test2.txt' },
24+
},
25+
]);
26+
const oldActivityBody = JSON.stringify([
27+
{
28+
type: 'file',
29+
props: { url: 'https://example.com/files/images/test.txt' },
30+
},
31+
{
32+
type: 'file',
33+
props: { url: 'https://example.com/files/images/test2.txt' },
34+
},
35+
]);
1736
const attachmentIdsToDelete = getActivityAttachmentIdsToDelete(
18-
'/files/attachment/test2.txt /files/attachment/test.txt',
37+
newActivityBody,
1938
attachments,
39+
oldActivityBody,
2040
);
2141
expect(attachmentIdsToDelete).toEqual([]);
2242
});
@@ -25,18 +45,34 @@ describe('getActivityAttachmentIdsToDelete', () => {
2545
const attachments = [
2646
{
2747
id: '1',
28-
fullPath: '/files/attachment/test.txt',
48+
fullPath: 'https://example.com/files/images/test.txt',
2949
},
3050
{
3151
id: '2',
32-
fullPath: '/files/attachment/test2.txt',
52+
fullPath: 'https://example.com/files/images/test2.txt',
3353
},
3454
] as Attachment[];
35-
55+
const newActivityBody = JSON.stringify([
56+
{
57+
type: 'file',
58+
props: { url: 'https://example.com/files/images/test.txt' },
59+
},
60+
]);
61+
const oldActivityBody = JSON.stringify([
62+
{
63+
type: 'file',
64+
props: { url: 'https://example.com/files/images/test.txt' },
65+
},
66+
{
67+
type: 'file',
68+
props: { url: 'https://example.com/files/images/test2.txt' },
69+
},
70+
]);
3671
const attachmentIdsToDelete = getActivityAttachmentIdsToDelete(
37-
'/files/attachment/test2.txt',
72+
newActivityBody,
3873
attachments,
74+
oldActivityBody,
3975
);
40-
expect(attachmentIdsToDelete).toEqual(['1']);
76+
expect(attachmentIdsToDelete).toEqual(['2']);
4177
});
4278
});

packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPaths.test.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)