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:
@@ -71,11 +71,9 @@ export const FileBlock = createReactBlockSpec(
|
||||
editor.updateBlock(block.id, {
|
||||
props: {
|
||||
...block.props,
|
||||
...{
|
||||
url: fileUrl,
|
||||
fileCategory: getFileType(file.name),
|
||||
name: file.name,
|
||||
},
|
||||
url: fileUrl,
|
||||
fileCategory: getFileType(file.name),
|
||||
name: file.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -139,10 +139,9 @@ export const ActivityRichTextEditor = ({
|
||||
if (!canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
persistBodyDebounced(prepareBodyWithSignedUrls(activityBody));
|
||||
},
|
||||
[persistBodyDebounced, setCanCreateActivity, canCreateActivity],
|
||||
[canCreateActivity, persistBodyDebounced, setCanCreateActivity],
|
||||
);
|
||||
|
||||
const handleBodyChange = useRecoilCallback(
|
||||
|
||||
@@ -15,11 +15,10 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FileIcon } from '@/file/components/FileIcon';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { isNavigationModifierPressed } from 'twenty-ui/utilities';
|
||||
import {
|
||||
@@ -107,11 +106,9 @@ export const AttachmentRow = ({
|
||||
: attachment.fileCategory;
|
||||
|
||||
const fileUrl = isFilesFieldMigrated
|
||||
? attachment.file?.[0]?.url
|
||||
? (attachment.file?.[0]?.url as string) // TODO : fix attachment.file type after Files field migration
|
||||
: attachment.fullPath;
|
||||
|
||||
assertIsDefinedOrThrow(fileUrl, new Error(t`File URL is not defined`));
|
||||
|
||||
const { destroyOneRecord: destroyOneAttachment } = useDestroyOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
});
|
||||
|
||||
+9
-2
@@ -50,6 +50,7 @@ export const useUploadAttachmentFile = () => {
|
||||
) => {
|
||||
let attachmentPath: string;
|
||||
let fileId: string | undefined;
|
||||
let fileUrl: string | undefined;
|
||||
|
||||
if (isFilesFieldMigrated) {
|
||||
assertIsDefinedOrThrow(
|
||||
@@ -69,6 +70,7 @@ export const useUploadAttachmentFile = () => {
|
||||
|
||||
attachmentPath = uploadedFile.path;
|
||||
fileId = uploadedFile.id;
|
||||
fileUrl = uploadedFile.url;
|
||||
} else {
|
||||
const result = await uploadFile({
|
||||
variables: {
|
||||
@@ -93,7 +95,7 @@ export const useUploadAttachmentFile = () => {
|
||||
|
||||
const attachmentToCreate = {
|
||||
name: file.name,
|
||||
fullPath: attachmentPath,
|
||||
fullPath: isFilesFieldMigrated ? null : attachmentPath,
|
||||
fileCategory: getFileType(file.name),
|
||||
[targetableObjectFieldIdName]: targetableObject.id,
|
||||
...(isFilesFieldMigrated && isDefined(fileId)
|
||||
@@ -110,7 +112,12 @@ export const useUploadAttachmentFile = () => {
|
||||
|
||||
const createdAttachment = await createOneAttachment(attachmentToCreate);
|
||||
|
||||
return { attachmentAbsoluteURL: createdAttachment.fullPath };
|
||||
return {
|
||||
attachmentAbsoluteURL: isFilesFieldMigrated
|
||||
? fileUrl
|
||||
: createdAttachment.fullPath,
|
||||
attachmentFileId: fileId,
|
||||
};
|
||||
};
|
||||
|
||||
return { uploadAttachmentFile };
|
||||
|
||||
+15
-10
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -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' }]);
|
||||
});
|
||||
});
|
||||
|
||||
+2
@@ -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']);
|
||||
});
|
||||
|
||||
+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',
|
||||
|
||||
-18
@@ -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);
|
||||
|
||||
+6
-1
@@ -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 });
|
||||
|
||||
+6
-1
@@ -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);
|
||||
|
||||
+6
-1
@@ -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;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ export const UPLOAD_FILES_FIELD_FILE = gql`
|
||||
path
|
||||
size
|
||||
createdAt
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -8,8 +8,13 @@ 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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useAttachmentSync = (attachments: Attachment[]) => {
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
const { deleteManyRecords: deleteAttachments } = useDeleteManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
});
|
||||
@@ -42,6 +47,7 @@ export const useAttachmentSync = (attachments: Attachment[]) => {
|
||||
newBody,
|
||||
attachments,
|
||||
previousBodyOrEmptyArray,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
|
||||
if (attachmentIdsToDelete.length > 0) {
|
||||
@@ -53,16 +59,18 @@ export const useAttachmentSync = (attachments: Attachment[]) => {
|
||||
const attachmentPathsToRestore = getActivityAttachmentPathsToRestore(
|
||||
newBody,
|
||||
attachments,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
|
||||
if (attachmentPathsToRestore.length > 0) {
|
||||
const softDeletedAttachments =
|
||||
(await findSoftDeletedAttachments()) as Attachment[];
|
||||
|
||||
const attachmentIdsToRestore = filterAttachmentsToRestore(
|
||||
const attachmentIdsToRestore = filterAttachmentsToRestore({
|
||||
attachmentPathsToRestore,
|
||||
softDeletedAttachments ?? [],
|
||||
);
|
||||
softDeletedAttachments: softDeletedAttachments ?? [],
|
||||
isFilesFieldMigrated,
|
||||
});
|
||||
|
||||
await restoreAttachments({
|
||||
idsToRestore: attachmentIdsToRestore,
|
||||
@@ -72,6 +80,7 @@ export const useAttachmentSync = (attachments: Attachment[]) => {
|
||||
const attachmentsToUpdate = getActivityAttachmentIdsAndNameToUpdate(
|
||||
newBody,
|
||||
attachments,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
|
||||
for (const attachmentToUpdate of attachmentsToUpdate) {
|
||||
|
||||
Reference in New Issue
Block a user