Add dashboard rollout commands (#15567)

## Tests

### makeSureDashboardNamingAvailableCommand

Case 1: no dashboard custom object
Case 2: with dashboard custom object

### SeedDashboardViewCommand

Case 1: no existing view
Case 2: with existing view
This commit is contained in:
Charles Bochet
2025-11-03 18:35:57 +01:00
committed by GitHub
parent 5bc876e4b4
commit 7ff91a61c6
8 changed files with 222 additions and 197 deletions
@@ -0,0 +1,81 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade:1-10:make-sure-dashboard-naming-available',
description: 'Make sure the dashboard naming is available',
})
export class MakeSureDashboardNamingAvailableCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const potentialCustomDashboardObjectMetadata =
await this.objectMetadataRepository.findOne({
where: {
nameSingular: 'dashboard',
workspaceId,
isCustom: true,
},
});
if (!isDefined(potentialCustomDashboardObjectMetadata)) {
this.logger.log(
`No custom dashboard object metadata found for workspace ${workspaceId}. Skipping...`,
);
return;
}
if (options.dryRun) {
this.logger.log(
`Would have updated the dashboard object metadata for workspace ${workspaceId}. Skipping...`,
);
return;
}
this.logger.log(
`Updating the dashboard object metadata for workspace ${workspaceId}...`,
);
await this.objectMetadataServiceV2.updateOne({
workspaceId,
updateObjectInput: {
id: potentialCustomDashboardObjectMetadata.id,
update: {
nameSingular: 'myDashboard',
namePlural: 'myDashboards',
labelSingular: 'My Dashboard',
labelPlural: 'My Dashboards',
},
},
});
this.logger.log(
`Updated the dashboard object metadata for workspace ${workspaceId}...`,
);
}
}
@@ -0,0 +1,115 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { createCoreViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views';
import { prefillWorkspaceFavorites } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workspace-favorites';
import { dashboardsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/dashboards-all.view';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
@Command({
name: 'upgrade:1-10:seed-dashboard-view',
description: 'Seed the dashboard view',
})
export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectDataSource()
private readonly coreDataSource: DataSource,
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(DataSourceEntity)
private readonly dataSourceRepository: Repository<DataSourceEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const [dashboardObjectMetadata] = await this.objectMetadataRepository.find({
where: {
workspaceId,
standardId: STANDARD_OBJECT_IDS.dashboard,
},
relations: ['fields'],
});
if (!isDefined(dashboardObjectMetadata)) {
throw new Error(
`Dashboard object metadata not found for workspace ${workspaceId}`,
);
}
const views = [dashboardsAllView([dashboardObjectMetadata], true)];
const schema = await this.dataSourceRepository.findOne({
where: {
workspaceId,
},
});
if (!isDefined(schema)) {
throw new Error(`Schema not found for workspace ${workspaceId}`);
}
const existingViews = await this.viewRepository.find({
where: {
workspaceId,
objectMetadataId: dashboardObjectMetadata.id,
key: ViewKey.INDEX,
},
});
if (existingViews.length > 0) {
this.logger.log(
`Dashboard view already exists for workspace ${workspaceId}. Skipping...`,
);
return;
}
if (options.dryRun) {
this.logger.log(
`Would have seeded dashboard view for workspace ${workspaceId}. Skipping...`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
const createdViews = await createCoreViews(queryRunner, workspaceId, views);
await prefillWorkspaceFavorites(
createdViews.map((view) => view.id),
queryRunner.manager as WorkspaceEntityManager,
schema.schema,
);
await queryRunner.release();
this.logger.log(
`Successfully seeded dashboard view for workspace ${workspaceId}: ${createdViews.map((view) => view.name).join(', ')}`,
);
}
}
@@ -4,14 +4,18 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command';
import { CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command';
import { CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command';
import { MakeSureDashboardNamingAvailableCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command';
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
import { MigrateChannelPartialFullSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command';
import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
import { SeedDashboardViewCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
@@ -23,8 +27,10 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
FieldMetadataEntity,
IndexMetadataEntity,
ViewEntity,
DataSourceEntity,
]),
WorkspaceSchemaManagerModule,
ObjectMetadataModule,
],
providers: [
MigrateChannelPartialFullSyncStagesCommand,
@@ -33,6 +39,8 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
RegenerateSearchVectorsCommand,
AddWorkflowRunStopStatusesCommand,
CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
MakeSureDashboardNamingAvailableCommand,
SeedDashboardViewCommand,
CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
],
exports: [
@@ -42,6 +50,8 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
AddWorkflowRunStopStatusesCommand,
CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
MigrateChannelPartialFullSyncStagesCommand,
MakeSureDashboardNamingAvailableCommand,
SeedDashboardViewCommand,
CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
],
})
@@ -12,10 +12,12 @@ import {
import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command';
import { CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command';
import { CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command';
import { MakeSureDashboardNamingAvailableCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command';
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
import { MigrateChannelPartialFullSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command';
import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
import { SeedDashboardViewCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command';
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
@@ -62,6 +64,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly addWorkflowRunStopStatusesCommand: AddWorkflowRunStopStatusesCommand,
protected readonly cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand: CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
protected readonly migrateChannelPartialFullSyncStagesCommand: MigrateChannelPartialFullSyncStagesCommand,
protected readonly makeSureDashboardNamingAvailableCommand: MakeSureDashboardNamingAvailableCommand,
protected readonly seedDashboardViewCommand: SeedDashboardViewCommand,
protected readonly createViewKanbanFieldMetadataIdForeignKeyMigrationCommand: CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
) {
super(
@@ -101,10 +105,12 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
this.createViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
this.migrateChannelPartialFullSyncStagesCommand,
this.makeSureDashboardNamingAvailableCommand,
],
afterSyncMetadata: [
this.migrateAttachmentAuthorToCreatedByCommand,
this.migrateAttachmentTypeToFileCategoryCommand,
this.seedDashboardViewCommand,
],
};