diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useExportNoteAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useExportNoteAction.ts index 649350ee4..3ad93026c 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useExportNoteAction.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useExportNoteAction.ts @@ -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( diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index 6b4992957..9ebf3b4d0 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -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); diff --git a/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts b/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts index 36f9096ac..ec5f05f6c 100644 --- a/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts @@ -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), - }, - }), - }, - }); + }; + }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivities.ts b/packages/twenty-front/src/modules/activities/hooks/useActivities.ts index b5311a800..d2d39056b 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivities.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivities.ts @@ -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 = ({ @@ -27,6 +29,10 @@ export const useActivities = ({ activitiesOrderByVariables: RecordGqlOperationOrderBy; skip?: boolean; }) => { + const isRichTextV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsRichTextV2Enabled, + ); + const { objectMetadataItems } = useObjectMetadataItems(); const { activityTargets, loadingActivityTargets } = @@ -64,6 +70,7 @@ export const useActivities = ({ findActivitiesOperationSignatureFactory({ objectMetadataItems, objectNameSingular, + isRichTextV2Enabled, }); const { records: activities, loading: loadingActivities } = diff --git a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts index e203711bb..16175e2a0 100644 --- a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts +++ b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts @@ -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({ diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command.ts new file mode 100644 index 000000000..b9584bdf8 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command.ts @@ -0,0 +1,187 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { In, Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { isCommandLogger } from 'src/database/commands/logger'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; +import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; + +@Command({ + name: 'upgrade-0.42:fix-body-v2-view-field-position', + description: 'Make bodyV2 field position to match body field position', +}) +export class FixBodyV2ViewFieldPositionCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log('Running command to fix bodyV2 field position'); + + if (isCommandLogger(this.logger)) { + this.logger.setVerbose(options.verbose ?? false); + } + + try { + for (const [index, workspaceId] of workspaceIds.entries()) { + await this.processWorkspace(workspaceId, index, workspaceIds.length); + } + + this.logger.log(chalk.green('Command completed!')); + } catch (error) { + this.logger.log(chalk.red('Error executing command')); + } + } + + private async processWorkspace( + workspaceId: string, + index: number, + total: number, + ): Promise { + try { + this.logger.log( + `Running command for workspace ${workspaceId} ${index + 1}/${total}`, + ); + + const viewRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'view', + ); + + const viewFieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'viewField', + false, + ); + + const taskAndNoteObjectMetadatas = + await this.objectMetadataRepository.find({ + where: { + workspaceId, + nameSingular: In(['note', 'task']), + }, + relations: ['fields'], + }); + + const taskAndNoteViews = await viewRepository.find({ + where: { + objectMetadataId: In( + taskAndNoteObjectMetadatas.map((object) => object.id), + ), + }, + }); + + const fieldMetadatas = taskAndNoteObjectMetadatas.flatMap( + (objectMetadata) => objectMetadata.fields, + ); + + const fieldNameByMetadataId: Record = + fieldMetadatas.reduce( + (fieldNameByMetadataId, fieldMetadata) => ({ + ...fieldNameByMetadataId, + [fieldMetadata.id]: fieldMetadata.name, + }), + {}, + ); + + for (const view of taskAndNoteViews) { + this.logger.log( + `Updating bodyV2 field position for view ${view.id} - ${view.name}`, + ); + const viewFields = await viewFieldRepository.find({ + where: { + viewId: view.id, + }, + }); + + const bodyViewField = viewFields.find( + (viewField) => + fieldNameByMetadataId[viewField.fieldMetadataId] === 'body', + ); + const bodyV2ViewField = viewFields.find( + (viewField) => + fieldNameByMetadataId[viewField.fieldMetadataId] === 'bodyV2', + ); + + if (bodyViewField && bodyV2ViewField) { + this.logger.log( + `Setting body field position to ${bodyV2ViewField?.position} and bodyV2 field position to ${bodyViewField?.position}`, + ); + + await viewFieldRepository.update( + { id: bodyViewField.id }, + { + position: bodyV2ViewField.position, + isVisible: false, + }, + ); + await viewFieldRepository.update( + { id: bodyV2ViewField.id }, + { + position: bodyViewField.position, + isVisible: bodyViewField.isVisible, + }, + ); + } else if (bodyViewField && !bodyV2ViewField) { + this.logger.log( + `Creating bodyV2 view field for view ${view.id} with position ${viewFields.length}`, + ); + + const bodyV2FieldMetadataId = fieldMetadatas.find( + (field) => field.name === 'bodyV2', + )?.id; + + await viewFieldRepository.create({ + fieldMetadataId: bodyV2FieldMetadataId, + viewId: view.id, + position: bodyViewField.position, + isVisible: bodyViewField.isVisible, + size: bodyViewField.size, + aggregateOperation: bodyViewField.aggregateOperation, + }); + + await viewFieldRepository.update( + { id: bodyViewField.id }, + { + position: viewFields.length, + isVisible: false, + }, + ); + } + } + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + + this.logger.log( + chalk.green(`Command completed for workspace ${workspaceId}`), + ); + } catch (error) { + this.logger.log(chalk.red(`Error in workspace ${workspaceId}`)); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command.ts new file mode 100644 index 000000000..92d2f6cb8 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command.ts @@ -0,0 +1,251 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { ServerBlockNoteEditor } from '@blocknote/server-util'; +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared'; +import { Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { isCommandLogger } from 'src/database/commands/logger'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnCreate, + WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; + +@Command({ + name: 'upgrade-0.42:migrate-rich-text-field', + description: 'Migrate RICH_TEXT fields to new composite structure', +}) +export class MigrateRichTextFieldCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + @InjectRepository(FeatureFlag, 'core') + protected readonly featureFlagRepository: Repository, + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceMigrationService: WorkspaceMigrationService, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log( + 'Running command to migrate RICH_TEXT fields to new composite structure', + ); + + if (isCommandLogger(this.logger)) { + this.logger.setVerbose(options.verbose ?? false); + } + + try { + for (const [index, workspaceId] of workspaceIds.entries()) { + await this.processWorkspace(workspaceId, index, workspaceIds.length); + } + + this.logger.log(chalk.green('Command completed!')); + } catch (error) { + this.logger.log(chalk.red('Error in workspace')); + } + } + + private async processWorkspace( + workspaceId: string, + index: number, + total: number, + ): Promise { + try { + this.logger.log( + `Running command for workspace ${workspaceId} ${index + 1}/${total}`, + ); + + const richTextFields = await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.RICH_TEXT, + }, + }); + + if (!richTextFields.length) { + this.logger.log( + chalk.yellow('No RICH_TEXT fields found in this workspace'), + ); + + return; + } + + this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`); + + for (const richTextField of richTextFields) { + await this.processRichTextField(richTextField, workspaceId); + } + + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, + ); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + + await this.migrateRichTextContent(richTextFields, workspaceId); + + await this.enableRichTextV2FeatureFlag(workspaceId); + + this.logger.log( + chalk.green(`Command completed for workspace ${workspaceId}`), + ); + } catch (error) { + this.logger.log(chalk.red(`Error in workspace ${workspaceId}: ${error}`)); + } + } + + private async enableRichTextV2FeatureFlag( + workspaceId: string, + ): Promise { + await this.featureFlagRepository.upsert( + { + workspaceId, + key: FeatureFlagKey.IsRichTextV2Enabled, + value: true, + }, + { + conflictPaths: ['workspaceId', 'key'], + skipUpdateIfNoValuesChanged: true, + }, + ); + } + + private async processRichTextField( + richTextField: FieldMetadataEntity, + workspaceId: string, + ) { + const newRichTextField: Partial = { + ...richTextField, + name: `${richTextField.name}V2`, + id: undefined, + type: FieldMetadataType.RICH_TEXT_V2, + defaultValue: null, + }; + + await this.fieldMetadataRepository.insert(newRichTextField); + + const objectMetadata = await this.objectMetadataRepository.findOne({ + where: { id: richTextField.objectMetadataId }, + }); + + if (objectMetadata === null) { + this.logger.log( + `Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`, + ); + + return; + } + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `migrate-rich-text-field-${objectMetadata.nameSingular}-${richTextField.name}`, + ), + workspaceId, + [ + { + name: computeObjectTargetTable(objectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${richTextField.name}V2Blocknote`, + columnType: 'text', + isNullable: true, + defaultValue: null, + } satisfies WorkspaceMigrationColumnCreate, + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: `${richTextField.name}V2Markdown`, + columnType: 'text', + isNullable: true, + defaultValue: null, + } satisfies WorkspaceMigrationColumnCreate, + ], + } satisfies WorkspaceMigrationTableAction, + ], + ); + } + + private async migrateRichTextContent( + richTextFields: FieldMetadataEntity[], + workspaceId: string, + ) { + const serverBlockNoteEditor = ServerBlockNoteEditor.create(); + + for (const richTextField of richTextFields) { + const objectMetadata = await this.objectMetadataRepository.findOne({ + where: { id: richTextField.objectMetadataId }, + }); + + if (objectMetadata === null) { + this.logger.log( + `Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`, + ); + continue; + } + + const schemaName = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( + workspaceId, + ); + + const rows = await workspaceDataSource.query( + `SELECT id, "${richTextField.name}" FROM "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}"`, + ); + + this.logger.log(`Generating markdown for ${rows.length} records`); + + for (const row of rows) { + const blocknoteFieldValue = row[richTextField.name]; + const markdownFieldValue = blocknoteFieldValue + ? await serverBlockNoteEditor.blocksToMarkdownLossy( + JSON.parse(blocknoteFieldValue), + ) + : null; + + await workspaceDataSource.query( + `UPDATE "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" SET "${richTextField.name}V2Blocknote" = $1, "${richTextField.name}V2Markdown" = $2 WHERE id = $3`, + [blocknoteFieldValue, markdownFieldValue, row.id], + ); + } + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command.ts index 47d13fa0b..933b22112 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command.ts @@ -5,7 +5,9 @@ import { Repository } from 'typeorm'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { BaseCommandOptions } from 'src/database/commands/base.command'; +import { FixBodyV2ViewFieldPositionCommand } from 'src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command'; import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field'; +import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Command({ @@ -16,6 +18,8 @@ export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner { constructor( @InjectRepository(Workspace, 'core') protected readonly workspaceRepository: Repository, + private readonly migrateRichTextFieldCommand: MigrateRichTextFieldCommand, + private readonly fixBodyV2ViewFieldPositionCommand: FixBodyV2ViewFieldPositionCommand, private readonly limitAmountOfViewFieldCommand: LimitAmountOfViewFieldCommand, ) { super(workspaceRepository); @@ -28,6 +32,18 @@ export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner { ): Promise { this.logger.log('Running command to upgrade to 0.42'); + await this.migrateRichTextFieldCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); + + await this.fixBodyV2ViewFieldPositionCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); + await this.limitAmountOfViewFieldCommand.executeActiveWorkspacesCommand( passedParam, options, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module.ts index 516394c52..1b622615e 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module.ts @@ -1,36 +1,37 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { FixBodyV2ViewFieldPositionCommand } from 'src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command'; import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field'; -import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command'; +import { UpgradeTo0_42Command } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command'; +import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; -import { SyncWorkspaceLoggerService } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.service'; -import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; -import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; -import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; @Module({ imports: [ - TypeOrmModule.forFeature([Workspace], 'core'), - TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), - TypeORMModule, - DataSourceModule, - ObjectMetadataModule, - WorkspaceSyncMetadataCommandsModule, - WorkspaceSyncMetadataModule, - WorkspaceHealthModule, - WorkspaceDataSourceModule, + TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), + WorkspaceMigrationRunnerModule, + WorkspaceMigrationModule, WorkspaceMetadataVersionModule, + WorkspaceDataSourceModule, + FeatureFlagModule, ], providers: [ - SyncWorkspaceLoggerService, - SyncWorkspaceMetadataCommand, + UpgradeTo0_42Command, + MigrateRichTextFieldCommand, + FixBodyV2ViewFieldPositionCommand, LimitAmountOfViewFieldCommand, ], }) diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunities.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunities.ts index 7723f6780..2a160ddc1 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunities.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunities.ts @@ -7,7 +7,7 @@ import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/worksp const tableName = 'opportunity'; export const DEV_SEED_OPPORTUNITY_IDS = { - OPPORTUNITY_1: '20202020-be10-412b-a663-16bd3c2228e1', + OPPORTUNITY_1: '20202020-be10-422b-a663-16bd3c2228e1', OPPORTUNITY_2: '20202020-0543-4cc2-9f96-95cc699960f2', OPPORTUNITY_3: '20202020-2f89-406f-90ea-180f433b2445', OPPORTUNITY_4: '20202020-35b1-4045-9cde-42f715148954', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index d66b58726..9e08fe18c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -325,7 +325,7 @@ export const OPPORTUNITY_STANDARD_FIELD_IDS = { favorites: '20202020-a1c2-4500-aaae-83ba8a0e827a', // TODO: check if activityTargets field can be deleted activityTargets: '20202020-220a-42d6-8261-b2102d6eab35', - taskTargets: '20202020-59c0-4179-a208-4a255f04a5be', + taskTargets: '20202020-59c0-4279-a208-4a255f04a5be', noteTargets: '20202020-dd3f-42d5-a382-db58aabf43d3', attachments: '20202020-87c7-4118-83d6-2f4031005209', timelineActivities: '20202020-30e2-421f-96c7-19c69d1cf631', @@ -338,7 +338,7 @@ export const PERSON_STANDARD_FIELD_IDS = { emails: '20202020-3c51-43fa-8b6e-af39e29368ab', linkedinLink: '20202020-f1af-48f7-893b-2007a73dd508', xLink: '20202020-8fc2-487c-b84a-55a99b145cfd', - jobTitle: '20202020-b0d0-415a-bef9-640a26dacd9b', + jobTitle: '20202020-b0d0-425a-bef9-640a26dacd9b', phone: '20202020-4564-4b8b-a09f-05445f2e0bce', phones: '20202020-0638-448e-8825-439134618022', city: '20202020-5243-4ffb-afc5-2c675da41346', @@ -380,7 +380,7 @@ export const TASK_TARGET_STANDARD_FIELD_IDS = { person: '20202020-c8a0-4e85-a016-87e2349cfbec', company: '20202020-4703-4a4e-948c-487b0c60a92c', opportunity: '20202020-6cb2-4c01-a9a5-aca3dbc11d41', - custom: '20202020-41c1-4c9a-8c75-be0971ef89af', + custom: '20202020-42c1-4c9a-8c75-be0971ef89af', }; export const VIEW_FIELD_STANDARD_FIELD_IDS = { diff --git a/packages/twenty-shared/src/types/FieldMetadataType.ts b/packages/twenty-shared/src/types/FieldMetadataType.ts index 686da7e67..c8cddee59 100644 --- a/packages/twenty-shared/src/types/FieldMetadataType.ts +++ b/packages/twenty-shared/src/types/FieldMetadataType.ts @@ -18,8 +18,8 @@ export enum FieldMetadataType { POSITION = 'POSITION', ADDRESS = 'ADDRESS', RAW_JSON = 'RAW_JSON', - RICH_TEXT_V2 = 'RICH_TEXT_V2', RICH_TEXT = 'RICH_TEXT', + RICH_TEXT_V2 = 'RICH_TEXT_V2', ACTOR = 'ACTOR', ARRAY = 'ARRAY', TS_VECTOR = 'TS_VECTOR',