Files - Migrate attachments in activities (#17808)

As attachment files have migrated from fullPath to file files field,
need to migrate richText logic to fit to new attachment file handling +
data migration
This commit is contained in:
Etienne
2026-02-13 10:11:23 +01:00
committed by GitHub
parent 90a30263ac
commit 5c2c588885
31 changed files with 798 additions and 141 deletions
@@ -9,18 +9,22 @@ describe('filterAttachmentsToRestore', () => {
fullPath: 'https://exemple.com/test.txt',
},
] as Attachment[];
const attachmentIdsToRestore = filterAttachmentsToRestore(
[],
const attachmentIdsToRestore = filterAttachmentsToRestore({
attachmentPathsToRestore: [],
softDeletedAttachments,
);
isFilesFieldMigrated: false,
});
expect(attachmentIdsToRestore).toEqual([]);
});
it('should not return any ids if there are no soft deleted attachments', () => {
const attachmentIdsToRestore = filterAttachmentsToRestore(
['https://exemple.com/files/attachment/test.txt'],
[],
);
const attachmentIdsToRestore = filterAttachmentsToRestore({
attachmentPathsToRestore: [
'https://exemple.com/files/attachment/test.txt',
],
softDeletedAttachments: [],
isFilesFieldMigrated: false,
});
expect(attachmentIdsToRestore).toEqual([]);
});
@@ -35,10 +39,11 @@ describe('filterAttachmentsToRestore', () => {
fullPath: 'https://exemple.com/files/images/test2.txt',
},
] as Attachment[];
const attachmentIdsToRestore = filterAttachmentsToRestore(
['https://exemple.com/files/images/test.txt'],
const attachmentIdsToRestore = filterAttachmentsToRestore({
attachmentPathsToRestore: ['https://exemple.com/files/images/test.txt'],
softDeletedAttachments,
);
isFilesFieldMigrated: false,
});
expect(attachmentIdsToRestore).toEqual(['1']);
});
});
@@ -33,7 +33,7 @@ describe('getActivityAttachmentIdsAndNameToUpdate', () => {
},
]);
const attachmentIdsAndNameToUpdate =
getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments);
getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments, false);
expect(attachmentIdsAndNameToUpdate).toEqual([]);
});
@@ -68,7 +68,7 @@ describe('getActivityAttachmentIdsAndNameToUpdate', () => {
},
]);
const attachmentIdsAndNameToUpdate =
getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments);
getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments, false);
expect(attachmentIdsAndNameToUpdate).toEqual([{ id: '2', name: 'image4' }]);
});
});
@@ -37,6 +37,7 @@ describe('getActivityAttachmentIdsToDelete', () => {
newActivityBody,
attachments,
oldActivityBody,
false,
);
expect(attachmentIdsToDelete).toEqual([]);
});
@@ -72,6 +73,7 @@ describe('getActivityAttachmentIdsToDelete', () => {
newActivityBody,
attachments,
oldActivityBody,
false,
);
expect(attachmentIdsToDelete).toEqual(['2']);
});
@@ -17,6 +17,7 @@ describe('getActivityAttachmentPathsToRestore', () => {
const attachmentPathsToRestore = getActivityAttachmentPathsToRestore(
newActivityBody,
oldActivityAttachments,
false,
);
expect(attachmentPathsToRestore).toEqual([]);
});
@@ -43,6 +44,7 @@ describe('getActivityAttachmentPathsToRestore', () => {
const attachmentPathsToRestore = getActivityAttachmentPathsToRestore(
newActivityBody,
oldActivityAttachments,
false,
);
expect(attachmentPathsToRestore).toEqual([
'https://example.com/files/images/test2.txt',
@@ -1,18 +0,0 @@
import { getAttachmentPath } from '@/activities/utils/getAttachmentPath';
describe('getAttachmentPath', () => {
it('should extract the correct path from a locally stored file URL with token', () => {
const token = 'dybszrrxvgrtefeidgybxzfxzr';
const res = getAttachmentPath(
`https://server.com/files/attachment/${token}/image.jpg?queryParam=value`,
);
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');
});
});
@@ -4,11 +4,15 @@ 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)
);
try {
const urlA = new URL(firstAttachmentUrl);
const urlB = new URL(secondAttachmentUrl);
if (urlA.hostname !== urlB.hostname) return false;
return (
getAttachmentPath(firstAttachmentUrl) ===
getAttachmentPath(secondAttachmentUrl)
);
} catch {
return firstAttachmentUrl === secondAttachmentUrl;
}
};
@@ -1,14 +1,23 @@
import { type Attachment } from '@/activities/files/types/Attachment';
import { compareUrls } from '@/activities/utils/compareUrls';
import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl';
export const filterAttachmentsToRestore = (
attachmentPathsToRestore: string[],
softDeletedAttachments: Attachment[],
) => {
export const filterAttachmentsToRestore = ({
attachmentPathsToRestore,
softDeletedAttachments,
isFilesFieldMigrated,
}: {
attachmentPathsToRestore: string[];
softDeletedAttachments: Attachment[];
isFilesFieldMigrated: boolean;
}) => {
return softDeletedAttachments
.filter((attachment) =>
attachmentPathsToRestore.some((path) =>
compareUrls(attachment.fullPath, path),
compareUrls(
getAttachmentUrl({ attachment, isFilesFieldMigrated }),
path,
),
),
)
.map((attachment) => attachment.id);
@@ -4,11 +4,13 @@ import {
type AttachmentInfo,
getActivityAttachmentPathsAndName,
} from '@/activities/utils/getActivityAttachmentPathsAndName';
import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl';
import { isDefined } from 'twenty-shared/utils';
export const getActivityAttachmentIdsAndNameToUpdate = (
newActivityBody: string,
oldActivityAttachments: Attachment[] = [],
isFilesFieldMigrated: boolean,
) => {
const activityAttachmentsNameAndPaths =
getActivityAttachmentPathsAndName(newActivityBody);
@@ -17,7 +19,10 @@ export const getActivityAttachmentIdsAndNameToUpdate = (
return activityAttachmentsNameAndPaths.reduce(
(acc: Partial<Attachment>[], activity: AttachmentInfo) => {
const foundActivity = oldActivityAttachments.find((attachment) =>
compareUrls(attachment.fullPath, activity.path),
compareUrls(
getAttachmentUrl({ attachment, isFilesFieldMigrated }),
activity.path,
),
);
if (isDefined(foundActivity) && foundActivity.name !== activity.name) {
acc.push({ id: foundActivity.id, name: activity.name });
@@ -1,11 +1,13 @@
import { type Attachment } from '@/activities/files/types/Attachment';
import { compareUrls } from '@/activities/utils/compareUrls';
import { getActivityAttachmentPathsAndName } from '@/activities/utils/getActivityAttachmentPathsAndName';
import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl';
export const getActivityAttachmentIdsToDelete = (
newActivityBody: string,
oldActivityAttachments: Attachment[] = [],
oldActivityBody: string,
isFilesFieldMigrated: boolean,
) => {
if (oldActivityAttachments.length === 0) return [];
@@ -27,7 +29,10 @@ export const getActivityAttachmentIdsToDelete = (
return oldActivityAttachments
.filter((attachment) =>
pathsToDelete.some((pathToDelete) =>
compareUrls(attachment.fullPath, pathToDelete),
compareUrls(
getAttachmentUrl({ attachment, isFilesFieldMigrated }),
pathToDelete,
),
),
)
.map((attachment) => attachment.id);
@@ -1,10 +1,12 @@
import { type Attachment } from '@/activities/files/types/Attachment';
import { compareUrls } from '@/activities/utils/compareUrls';
import { getActivityAttachmentPathsAndName } from '@/activities/utils/getActivityAttachmentPathsAndName';
import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl';
export const getActivityAttachmentPathsToRestore = (
newActivityBody: string,
oldActivityAttachments: Attachment[],
isFilesFieldMigrated: boolean,
) => {
const newActivityAttachmentPaths =
getActivityAttachmentPathsAndName(newActivityBody);
@@ -13,7 +15,10 @@ export const getActivityAttachmentPathsToRestore = (
.filter(
(newActivity) =>
!oldActivityAttachments.some((attachment) =>
compareUrls(newActivity.path, attachment.fullPath),
compareUrls(
newActivity.path,
getAttachmentUrl({ attachment, isFilesFieldMigrated }),
),
),
)
.map((activity) => activity.path);
@@ -1,4 +1,8 @@
export const getAttachmentPath = (attachmentFullPath: string) => {
if (attachmentFullPath.includes('/files-field/')) {
return attachmentFullPath?.split('?')[0];
}
if (!attachmentFullPath.includes('/files/')) {
return attachmentFullPath?.split('?')[0];
}
@@ -0,0 +1,16 @@
import { type Attachment } from '@/activities/files/types/Attachment';
export const getAttachmentUrl = ({
attachment,
isFilesFieldMigrated,
}: {
attachment: Attachment;
isFilesFieldMigrated: boolean;
}): string => {
if (isFilesFieldMigrated) {
//TODO : add minimumFile settings + set it for attachment.file files field + exception invariance check here
return attachment.file?.[0]?.url as string;
}
return attachment.fullPath as string;
};