diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command.ts new file mode 100644 index 0000000000..aa594d67df --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command.ts @@ -0,0 +1,219 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { ViewType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; + +@Command({ + name: 'upgrade:1-19:backfill-page-layouts', + description: + 'Backfill RECORD_PAGE page layouts for legacy workspaces and enable the IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED feature flag', +}) +export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + private readonly applicationService: ApplicationService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly featureFlagService: FeatureFlagService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun ?? false; + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of page layouts for workspace ${workspaceId}`, + ); + + const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED, + workspaceId, + ); + + if (isAlreadyEnabled) { + this.logger.log( + `IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`, + ); + + return; + } + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Would create RECORD_PAGE page layouts and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`, + ); + + return; + } + + const { twentyStandardFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const { allFlatEntityMaps: standardAllFlatEntityMaps } = + computeTwentyStandardApplicationAllFlatEntityMaps({ + shouldIncludeRecordPageLayouts: true, + now: new Date().toISOString(), + workspaceId, + twentyStandardApplicationId: twentyStandardFlatApplication.id, + }); + + const recordPageLayoutUniversalIdentifiers = new Set(); + + const pageLayoutsToCreate = Object.values( + standardAllFlatEntityMaps.flatPageLayoutMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((pageLayout) => { + if (pageLayout.type !== PageLayoutType.RECORD_PAGE) { + return false; + } + + recordPageLayoutUniversalIdentifiers.add( + pageLayout.universalIdentifier, + ); + + return true; + }); + + const tabUniversalIdentifiers = new Set(); + + const pageLayoutTabsToCreate = Object.values( + standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((tab) => { + if ( + !recordPageLayoutUniversalIdentifiers.has( + tab.pageLayoutUniversalIdentifier, + ) + ) { + return false; + } + + tabUniversalIdentifiers.add(tab.universalIdentifier); + + return true; + }); + + const pageLayoutWidgetsToCreate = Object.values( + standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((widget) => + tabUniversalIdentifiers.has(widget.pageLayoutTabUniversalIdentifier), + ); + + const viewUniversalIdentifiers = new Set(); + + const viewsToCreate = Object.values( + standardAllFlatEntityMaps.flatViewMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((view) => { + if (view.type !== ViewType.FIELDS_WIDGET) { + return false; + } + + viewUniversalIdentifiers.add(view.universalIdentifier); + + return true; + }); + + const viewFieldsToCreate = Object.values( + standardAllFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((viewField) => + viewUniversalIdentifiers.has(viewField.viewUniversalIdentifier), + ); + + const viewFieldGroupsToCreate = Object.values( + standardAllFlatEntityMaps.flatViewFieldGroupMaps.byUniversalIdentifier, + ) + .filter(isDefined) + .filter((viewFieldGroup) => + viewUniversalIdentifiers.has(viewFieldGroup.viewUniversalIdentifier), + ); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + pageLayout: { + flatEntityToCreate: pageLayoutsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + pageLayoutTab: { + flatEntityToCreate: pageLayoutTabsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + pageLayoutWidget: { + flatEntityToCreate: pageLayoutWidgetsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + view: { + flatEntityToCreate: viewsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + viewField: { + flatEntityToCreate: viewFieldsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + viewFieldGroup: { + flatEntityToCreate: viewFieldGroupsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier: + twentyStandardFlatApplication.universalIdentifier, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + this.logger.error( + `Failed to create page layouts:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, + ); + throw new Error( + `Failed to create page layouts for workspace ${workspaceId}`, + ); + } + + await this.featureFlagService.enableFeatureFlags( + [FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED], + workspaceId, + ); + + this.logger.log( + `Successfully created page layouts and enabled IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module.ts index 28136d2645..f95bdff625 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module.ts @@ -3,8 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AddMissingSystemFieldsToStandardObjectsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command'; import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command'; +import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command'; import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-system-fields-is-system.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; @@ -23,16 +25,19 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace WorkspaceMigrationRunnerModule, ApplicationModule, WorkspaceMigrationModule, + FeatureFlagModule, ], providers: [ BackfillSystemFieldsIsSystemCommand, AddMissingSystemFieldsToStandardObjectsCommand, BackfillMessageChannelMessageAssociationMessageFolderCommand, + BackfillPageLayoutsCommand, ], exports: [ BackfillSystemFieldsIsSystemCommand, AddMissingSystemFieldsToStandardObjectsCommand, BackfillMessageChannelMessageAssociationMessageFolderCommand, + BackfillPageLayoutsCommand, ], }) export class V1_19_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index b60470b05d..0a272543aa 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -28,6 +28,7 @@ import { MigrateWorkflowSendEmailAttachmentsCommand } from 'src/database/command import { MigrateWorkspacePicturesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command'; import { AddMissingSystemFieldsToStandardObjectsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command'; import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command'; +import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command'; import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-system-fields-is-system.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -73,6 +74,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly backfillSystemFieldsIsSystemCommand: BackfillSystemFieldsIsSystemCommand, protected readonly addMissingSystemFieldsToStandardObjectsCommand: AddMissingSystemFieldsToStandardObjectsCommand, protected readonly backfillMessageChannelMessageAssociationMessageFolderCommand: BackfillMessageChannelMessageAssociationMessageFolderCommand, + protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand, ) { super( workspaceRepository, @@ -112,6 +114,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.backfillSystemFieldsIsSystemCommand, this.addMissingSystemFieldsToStandardObjectsCommand, this.backfillMessageChannelMessageAssociationMessageFolderCommand, + this.backfillPageLayoutsCommand, ]; this.allCommands = {