RICH_TEXT_V2 upgrade command (#10094)

Adds two migration commands:
- copy note and task `body` data to `bodyV2`
- hide `body` view field and swap position with `bodyV2` view field

Related to issue https://github.com/twentyhq/twenty/issues/7613

---------

Co-authored-by: ad-elias <elias@autodiligence.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
eliasylonen
2025-02-12 12:26:29 +01:00
committed by GitHub
parent 33af71ccd3
commit 23d2e54439
12 changed files with 605 additions and 78 deletions

View File

@ -2,9 +2,11 @@ import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { BlockNoteEditor } from '@blocknote/core';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated/graphql';
export const useExportNoteAction: ActionHookWithObjectMetadataItem = ({
objectMetadataItem,
@ -22,13 +24,35 @@ export const useExportNoteAction: ActionHookWithObjectMetadataItem = ({
const shouldBeRegistered =
isDefined(objectMetadataItem) && isDefined(selectedRecord) && isNoteOrTask;
const isRichTextV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsRichTextV2Enabled,
);
const onClick = async () => {
if (!shouldBeRegistered || !selectedRecord?.body) {
return;
}
const editor = await BlockNoteEditor.create({
initialContent: JSON.parse(selectedRecord.body),
const initialBody = isRichTextV2Enabled
? selectedRecord.bodyV2?.blocknote
: selectedRecord.body;
let parsedBody = [];
// TODO: Remove this once we have removed the old rich text
try {
parsedBody = JSON.parse(initialBody);
} catch (error) {
// eslint-disable-next-line no-console
console.warn(
`Failed to parse body for record ${recordId}, for rich text version ${isRichTextV2Enabled ? 'v2' : 'v1'}`,
);
// eslint-disable-next-line no-console
console.warn(initialBody);
}
const editor = BlockNoteEditor.create({
initialContent: parsedBody,
});
const { exportBlockNoteEditorToPdf } = await import(

View File

@ -1,4 +1,5 @@
import { useApolloClient } from '@apollo/client';
import { PartialBlock } from '@blocknote/core';
import { useCreateBlockNote } from '@blocknote/react';
import { isArray, isNonEmptyString } from '@sniptt/guards';
import { useCallback, useMemo } from 'react';
@ -183,9 +184,29 @@ export const ActivityRichTextEditor = ({
isNonEmptyString(blocknote) &&
blocknote !== '{}'
) {
return JSON.parse(blocknote);
let parsedBody: PartialBlock[] | undefined = undefined;
// TODO: Remove this once we have removed the old rich text
try {
parsedBody = JSON.parse(blocknote);
} catch (error) {
// eslint-disable-next-line no-console
console.warn(
`Failed to parse body for activity ${activityId}, for rich text version ${isRichTextV2Enabled ? 'v2' : 'v1'}`,
);
// eslint-disable-next-line no-console
console.warn(blocknote);
}
if (isArray(parsedBody) && parsedBody.length === 0) {
return undefined;
}
return parsedBody;
}
}, [activity, isRichTextV2Enabled]);
return undefined;
}, [activity, isRichTextV2Enabled, activityId]);
const handleEditorBuiltInUploadFile = async (file: File) => {
const { attachmentAbsoluteURL } = await handleUploadAttachment(file);

View File

@ -7,59 +7,72 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
({
objectMetadataItems,
objectNameSingular,
isRichTextV2Enabled,
}: {
objectMetadataItems: ObjectMetadataItem[];
objectNameSingular: CoreObjectNameSingular;
}) => ({
objectNameSingular: objectNameSingular,
variables: {},
fields: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
author: {
isRichTextV2Enabled: boolean;
}) => {
const body = isRichTextV2Enabled
? {
bodyV2: {
markdown: true,
blocknote: true,
},
}
: { body: true };
return {
objectNameSingular: objectNameSingular,
variables: {},
fields: {
id: true,
name: true,
__typename: true,
createdAt: true,
updatedAt: true,
author: {
id: true,
name: true,
__typename: true,
},
authorId: true,
assigneeId: true,
assignee: {
id: true,
name: true,
__typename: true,
},
comments: true,
attachments: true,
...body,
title: true,
status: true,
dueAt: true,
reminderAt: true,
type: true,
...(objectNameSingular === CoreObjectNameSingular.Note
? {
noteTargets: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
note: true,
noteId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
},
}
: {
taskTargets: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
task: true,
taskId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
},
}),
},
authorId: true,
assigneeId: true,
assignee: {
id: true,
name: true,
__typename: true,
},
comments: true,
attachments: true,
body: true,
title: true,
status: true,
dueAt: true,
reminderAt: true,
type: true,
...(objectNameSingular === CoreObjectNameSingular.Note
? {
noteTargets: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
note: true,
noteId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
},
}
: {
taskTargets: {
id: true,
__typename: true,
createdAt: true,
updatedAt: true,
task: true,
taskId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
},
}),
},
});
};
};

View File

@ -12,6 +12,8 @@ import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGq
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const useActivities = <T extends Task | Note>({
@ -27,6 +29,10 @@ export const useActivities = <T extends Task | Note>({
activitiesOrderByVariables: RecordGqlOperationOrderBy;
skip?: boolean;
}) => {
const isRichTextV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsRichTextV2Enabled,
);
const { objectMetadataItems } = useObjectMetadataItems();
const { activityTargets, loadingActivityTargets } =
@ -64,6 +70,7 @@ export const useActivities = <T extends Task | Note>({
findActivitiesOperationSignatureFactory({
objectMetadataItems,
objectNameSingular,
isRichTextV2Enabled,
});
const { records: activities, loading: loadingActivities } =

View File

@ -13,7 +13,9 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const usePrepareFindManyActivitiesQuery = ({
@ -21,6 +23,10 @@ export const usePrepareFindManyActivitiesQuery = ({
}: {
activityObjectNameSingular: CoreObjectNameSingular;
}) => {
const isRichTextV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsRichTextV2Enabled,
);
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({
objectNameSingular: activityObjectNameSingular,
@ -114,6 +120,7 @@ export const usePrepareFindManyActivitiesQuery = ({
findActivitiesOperationSignatureFactory({
objectNameSingular: activityObjectNameSingular,
objectMetadataItems,
isRichTextV2Enabled,
});
upsertFindManyActivitiesInCache({