[Requires "warm" cache flush (no immediate downtime before flush)] Migrate viewGroup.fieldMetadataId -> view.mainGroupByFieldMetadataId (1/3) (#16206)
In this PR (1/3) - introduce view.mainGroupByFieldMetadataId as the new reference determining which fieldMetadataId is used in a grouped view, in order to deprecate viewGroup.fieldMetadataId which creates inconsistencies. view.mainGroupByFieldMetadataId is now filled at every view creation, though not in use yet. - Introduce a command to backfill view.mainGroupByFieldMetadataId for existing views + delete all viewGroup.fieldMetadataId with a fieldMetadataId that is not view.mainGroupByFieldMetadataId. (It should concern 37 active workspaces) - Temporarily disable the option to change a grouped view's fieldMetadataId as for now it creates inconsistencies. This feature can be reintroduced when we have done the full migration. In a next PR - (2/3) use view.mainGroupByFieldMetadataId instead of viewGroup.fieldMetadataId. In FE we may keep viewGroup.fieldMetadataId as a state (TBD). View groups will now be created / deleted as a side effect of view's mainGroupByFieldMetadataId update. - (3/3) remove viewGroup.fieldMetadataId --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { MigrationCommandOptions } from 'src/database/commands/command-runners/migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:backfill:view-main-group-by-field-metadata-id',
|
||||
description:
|
||||
'Backfill mainGroupByFieldMetadataId on views and clean up inconsistent viewGroups',
|
||||
})
|
||||
export class BackfillViewMainGroupByFieldMetadataIdCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
@InjectRepository(ViewGroupEntity)
|
||||
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
await this.backfillMainGroupByFieldMetadataId(workspaceId, options);
|
||||
await this.cleanupInconsistentViewGroups(workspaceId, options);
|
||||
}
|
||||
|
||||
private async backfillMainGroupByFieldMetadataId(
|
||||
workspaceId: string,
|
||||
options: MigrationCommandOptions,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`Starting backfill of mainGroupByFieldMetadataId for workspace ${workspaceId}...`,
|
||||
);
|
||||
|
||||
const viewsToBackfill = await this.viewRepository.find({
|
||||
where: {
|
||||
mainGroupByFieldMetadataId: IsNull(),
|
||||
workspaceId,
|
||||
},
|
||||
select: ['id', 'workspaceId'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${viewsToBackfill.length} views with null mainGroupByFieldMetadataId`,
|
||||
);
|
||||
|
||||
let backfilledCount = 0;
|
||||
let inconsistentViewsCount = 0;
|
||||
|
||||
for (const view of viewsToBackfill) {
|
||||
const nonDeletedViewGroups = await this.viewGroupRepository.find({
|
||||
where: {
|
||||
viewId: view.id,
|
||||
deletedAt: IsNull(),
|
||||
workspaceId,
|
||||
},
|
||||
select: ['id', 'fieldMetadataId'],
|
||||
});
|
||||
|
||||
if (nonDeletedViewGroups.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uniqueFieldMetadataIds = [
|
||||
...new Set(nonDeletedViewGroups.map((vg) => vg.fieldMetadataId)),
|
||||
];
|
||||
|
||||
if (uniqueFieldMetadataIds.length === 1) {
|
||||
const fieldMetadataId = uniqueFieldMetadataIds[0];
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would backfill view ${view.id} (workspace ${view.workspaceId}) with fieldMetadataId ${fieldMetadataId}`,
|
||||
);
|
||||
} else {
|
||||
await this.viewRepository.update(
|
||||
{ id: view.id, workspaceId },
|
||||
{ mainGroupByFieldMetadataId: fieldMetadataId },
|
||||
);
|
||||
this.logger.log(
|
||||
`Backfilled view ${view.id} (workspace ${view.workspaceId}) with fieldMetadataId ${fieldMetadataId}`,
|
||||
);
|
||||
}
|
||||
backfilledCount++;
|
||||
} else {
|
||||
this.logger.error(
|
||||
`Inconsistency detected for view ${view.id} (workspace ${view.workspaceId}): found ${uniqueFieldMetadataIds.length} different fieldMetadataIds`,
|
||||
);
|
||||
|
||||
const fieldMetadataIdCounts = new Map<string, number>();
|
||||
|
||||
for (const vg of nonDeletedViewGroups) {
|
||||
const count = fieldMetadataIdCounts.get(vg.fieldMetadataId) || 0;
|
||||
|
||||
fieldMetadataIdCounts.set(vg.fieldMetadataId, count + 1);
|
||||
}
|
||||
|
||||
let mostNumerousFieldMetadataId = '';
|
||||
let maxCount = 0;
|
||||
|
||||
for (const [
|
||||
fieldMetadataId,
|
||||
count,
|
||||
] of fieldMetadataIdCounts.entries()) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
mostNumerousFieldMetadataId = fieldMetadataId;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would backfill view ${view.id} (workspace ${view.workspaceId}) with fieldMetadataId ${mostNumerousFieldMetadataId} (most numerous, found ${maxCount} occurrences)`,
|
||||
);
|
||||
} else {
|
||||
await this.viewRepository.update(
|
||||
{ id: view.id },
|
||||
{ mainGroupByFieldMetadataId: mostNumerousFieldMetadataId },
|
||||
);
|
||||
this.logger.log(
|
||||
`Backfilled view ${view.id} (workspace ${view.workspaceId}) with fieldMetadataId ${mostNumerousFieldMetadataId} (most numerous, found ${maxCount} occurrences)`,
|
||||
);
|
||||
}
|
||||
backfilledCount++;
|
||||
inconsistentViewsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${options.dryRun ? '[DRY RUN] Would have ' : ''}Backfilled ${backfilledCount} views${inconsistentViewsCount > 0 ? ` (${inconsistentViewsCount} with inconsistencies)` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async cleanupInconsistentViewGroups(
|
||||
workspaceId: string,
|
||||
options: MigrationCommandOptions,
|
||||
): Promise<void> {
|
||||
this.logger.log('Starting cleanup of inconsistent viewGroups...');
|
||||
|
||||
const viewsWithMainGroupBy = await this.viewRepository.find({
|
||||
where: {
|
||||
mainGroupByFieldMetadataId: Not(IsNull()),
|
||||
workspaceId,
|
||||
},
|
||||
select: ['id', 'mainGroupByFieldMetadataId'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${viewsWithMainGroupBy.length} views with mainGroupByFieldMetadataId set`,
|
||||
);
|
||||
|
||||
let totalDeletedCount = 0;
|
||||
|
||||
for (const view of viewsWithMainGroupBy) {
|
||||
if (!isDefined(view.mainGroupByFieldMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const inconsistentViewGroups = await this.viewGroupRepository.find({
|
||||
where: {
|
||||
viewId: view.id,
|
||||
fieldMetadataId: Not(view.mainGroupByFieldMetadataId),
|
||||
workspaceId,
|
||||
},
|
||||
select: ['id'],
|
||||
});
|
||||
|
||||
if (inconsistentViewGroups.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewGroupIds = inconsistentViewGroups.map((vg) => vg.id);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would delete ${inconsistentViewGroups.length} viewGroups from view ${view.id}`,
|
||||
);
|
||||
} else {
|
||||
await this.viewGroupRepository.delete({
|
||||
id: In(viewGroupIds),
|
||||
workspaceId,
|
||||
});
|
||||
this.logger.log(
|
||||
`Deleted ${inconsistentViewGroups.length} viewGroups from view ${view.id}`,
|
||||
);
|
||||
}
|
||||
totalDeletedCount += inconsistentViewGroups.length;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${options.dryRun ? '[DRY RUN] Would have ' : ''}Deleted ${totalDeletedCount} inconsistent viewGroups`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -1,11 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillViewMainGroupByFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-view-main-group-by-field-metadata-id.command';
|
||||
import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command';
|
||||
import { WorkspaceEntity } 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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -13,10 +16,18 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
WorkspaceEntity,
|
||||
ObjectMetadataEntity,
|
||||
FieldMetadataEntity,
|
||||
ViewEntity,
|
||||
ViewGroupEntity,
|
||||
]),
|
||||
DataSourceModule,
|
||||
],
|
||||
providers: [CleanEmptyStringNullInTextFieldsCommand],
|
||||
exports: [CleanEmptyStringNullInTextFieldsCommand],
|
||||
providers: [
|
||||
CleanEmptyStringNullInTextFieldsCommand,
|
||||
BackfillViewMainGroupByFieldMetadataIdCommand,
|
||||
],
|
||||
exports: [
|
||||
CleanEmptyStringNullInTextFieldsCommand,
|
||||
BackfillViewMainGroupByFieldMetadataIdCommand,
|
||||
],
|
||||
})
|
||||
export class V1_13_UpgradeVersionCommandModule {}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddMainGroupByFieldMetadataId1764680275312
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddMainGroupByFieldMetadataId1764680275312';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."view" ADD "mainGroupByFieldMetadataId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."view" ADD CONSTRAINT "FK_d1fa625016e36ec6f79fb13e824" FOREIGN KEY ("mainGroupByFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."view" DROP CONSTRAINT "FK_d1fa625016e36ec6f79fb13e824"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."view" DROP COLUMN "mainGroupByFieldMetadataId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
@@ -1443,6 +1443,9 @@ export const STANDARD_OBJECTS = {
|
||||
universalIdentifier:
|
||||
VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperationFieldMetadataId,
|
||||
},
|
||||
mainGroupByFieldMetadataId: {
|
||||
universalIdentifier: VIEW_STANDARD_FIELD_IDS.mainGroupByFieldMetadataId,
|
||||
},
|
||||
position: { universalIdentifier: VIEW_STANDARD_FIELD_IDS.position },
|
||||
isCompact: { universalIdentifier: VIEW_STANDARD_FIELD_IDS.isCompact },
|
||||
openRecordIn: {
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
FieldMetadataType,
|
||||
FieldMetadataSettings,
|
||||
FieldMetadataOptions,
|
||||
FieldMetadataSettings,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
Check,
|
||||
@@ -212,4 +212,7 @@ export class FieldMetadataEntity<
|
||||
|
||||
@OneToMany(() => ViewEntity, (view) => view.calendarFieldMetadata)
|
||||
calendarViews: Relation<ViewEntity[]>;
|
||||
|
||||
@OneToMany(() => ViewEntity, (view) => view.mainGroupByFieldMetadata)
|
||||
mainGroupByFieldMetadataViews: Relation<ViewEntity[]>;
|
||||
}
|
||||
|
||||
+4
@@ -41,6 +41,10 @@ export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'calendarViewIds',
|
||||
},
|
||||
mainGroupByFieldMetadataId: {
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'mainGroupByFieldMetadataViewIds',
|
||||
},
|
||||
objectMetadataId: {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: 'viewIds',
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFieldIds: [],
|
||||
createdAt,
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
updatedAt: createdAt,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export const getRelationTargetFlatFieldMetadataMock = ({
|
||||
|
||||
return {
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
viewFilterIds: [],
|
||||
viewGroupIds: [],
|
||||
viewFieldIds: [],
|
||||
|
||||
+4
@@ -120,6 +120,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "newFieldLabel",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": Any<String>,
|
||||
"name": "newFieldPets",
|
||||
"objectMetadataId": Any<String>,
|
||||
@@ -156,6 +157,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "Pet",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "pet",
|
||||
"objectMetadataId": Any<String>,
|
||||
@@ -194,6 +196,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "newFieldLabel",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": Any<String>,
|
||||
"name": "newFieldCompanies",
|
||||
"objectMetadataId": Any<String>,
|
||||
@@ -230,6 +233,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isUnique": null,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "Company",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "company",
|
||||
"objectMetadataId": Any<String>,
|
||||
|
||||
+9
@@ -68,6 +68,7 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
|
||||
'id',
|
||||
'kanbanAggregateOperationFieldMetadataId',
|
||||
'calendarFieldMetadataId',
|
||||
'mainGroupByFieldMetadataId',
|
||||
],
|
||||
withDeleted: true,
|
||||
}),
|
||||
@@ -79,6 +80,7 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
|
||||
viewGroupsByFieldId,
|
||||
calendarViewsByFieldId,
|
||||
kanbanViewsByFieldId,
|
||||
mainGroupByFieldMetadataViewsByFieldId,
|
||||
] = (
|
||||
[
|
||||
{
|
||||
@@ -101,6 +103,10 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
|
||||
entities: views,
|
||||
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
|
||||
},
|
||||
{
|
||||
entities: views,
|
||||
foreignKey: 'mainGroupByFieldMetadataId',
|
||||
},
|
||||
] as const
|
||||
).map(regroupEntitiesByRelatedEntityId);
|
||||
|
||||
@@ -115,6 +121,9 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro
|
||||
kanbanAggregateOperationViews:
|
||||
kanbanViewsByFieldId.get(fieldMetadataEntity.id) || [],
|
||||
calendarViews: calendarViewsByFieldId.get(fieldMetadataEntity.id) || [],
|
||||
mainGroupByFieldMetadataViews:
|
||||
mainGroupByFieldMetadataViewsByFieldId.get(fieldMetadataEntity.id) ||
|
||||
[],
|
||||
} as FieldMetadataEntity);
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
|
||||
+4
-2
@@ -11,9 +11,10 @@ export const FIELD_METADATA_RELATION_PROPERTIES = [
|
||||
'viewFields',
|
||||
'application',
|
||||
'viewFilters',
|
||||
'viewGroups',
|
||||
'kanbanAggregateOperationViews',
|
||||
'calendarViews',
|
||||
'mainGroupByFieldMetadataViews',
|
||||
'viewGroups',
|
||||
] as const satisfies (keyof FieldMetadataEntity)[];
|
||||
|
||||
export type FieldMetadataEntityRelationProperties =
|
||||
@@ -24,7 +25,8 @@ export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
|
||||
universalIdentifier: string;
|
||||
viewFieldIds: string[];
|
||||
viewFilterIds: string[];
|
||||
viewGroupIds: string[];
|
||||
kanbanAggregateOperationViewIds: string[];
|
||||
calendarViewIds: string[];
|
||||
mainGroupByFieldMetadataViewIds: string[];
|
||||
viewGroupIds: string[];
|
||||
};
|
||||
|
||||
+3
@@ -23,6 +23,9 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
|
||||
kanbanAggregateOperationViewIds:
|
||||
fieldMetadataEntity.kanbanAggregateOperationViews.map(({ id }) => id),
|
||||
calendarViewIds: fieldMetadataEntity.calendarViews.map(({ id }) => id),
|
||||
mainGroupByFieldMetadataViewIds:
|
||||
fieldMetadataEntity.mainGroupByFieldMetadataViews?.map(({ id }) => id) ??
|
||||
[],
|
||||
viewGroupIds: fieldMetadataEntity.viewGroups.map(({ id }) => id),
|
||||
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
|
||||
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
|
||||
return {
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
viewFieldIds: [],
|
||||
description: createFieldInput.description ?? null,
|
||||
id: fieldMetadataId,
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { type EnumFieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
@@ -63,7 +63,8 @@ export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({
|
||||
const flatViewGroupsToUpdate = updatedFieldMetadataOptions.flatMap(
|
||||
({ from: fromOption, to: toOption }) =>
|
||||
flatViewGroups.flatMap((flatViewGroup) =>
|
||||
flatViewGroup.fieldValue === fromOption.value
|
||||
flatViewGroup.fieldValue === fromOption.value &&
|
||||
flatViewGroup.fieldMetadataId === fromFlatFieldMetadata.id
|
||||
? { ...flatViewGroup, fieldValue: toOption.value }
|
||||
: [],
|
||||
),
|
||||
|
||||
+1
@@ -13,4 +13,5 @@ export const FLAT_VIEW_EDITABLE_PROPERTIES = [
|
||||
'calendarLayout',
|
||||
'calendarFieldMetadataId',
|
||||
'visibility',
|
||||
'mainGroupByFieldMetadataId',
|
||||
] as const satisfies (keyof FlatView)[];
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ export const fromCreateViewInputToFlatViewToCreate = ({
|
||||
kanbanAggregateOperation: createViewInput.kanbanAggregateOperation ?? null,
|
||||
kanbanAggregateOperationFieldMetadataId:
|
||||
createViewInput.kanbanAggregateOperationFieldMetadataId ?? null,
|
||||
mainGroupByFieldMetadataId:
|
||||
createViewInput.mainGroupByFieldMetadataId ?? null,
|
||||
key: createViewInput.key ?? null,
|
||||
openRecordIn: createViewInput.openRecordIn ?? ViewOpenRecordIn.SIDE_PANEL,
|
||||
position: createViewInput.position ?? 0,
|
||||
|
||||
+8
@@ -31,6 +31,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: true,
|
||||
objectMetadataId,
|
||||
@@ -68,6 +69,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -105,6 +107,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -142,6 +145,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -179,6 +183,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -216,6 +221,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -252,6 +258,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
objectMetadataId,
|
||||
@@ -284,6 +291,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
const searchVectorFieldId = v4();
|
||||
const searchVectorField: FlatFieldMetadata<FieldMetadataType.TS_VECTOR> = {
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
viewFieldIds: [],
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
|
||||
+2
@@ -61,6 +61,7 @@ const generateSourceFlatFieldMetadata = ({
|
||||
|
||||
return {
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFilterIds: [],
|
||||
viewFieldIds: [],
|
||||
@@ -128,6 +129,7 @@ const generateTargetFlatFieldMetadata = ({
|
||||
return {
|
||||
morphId: null,
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
viewFieldIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFilterIds: [],
|
||||
|
||||
+8
-3
@@ -69,7 +69,7 @@ export class ViewFilterService {
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
const buildAndRunResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
fromToAllFlatEntityMaps: {
|
||||
@@ -92,13 +92,18 @@ export class ViewFilterService {
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
if (isDefined(buildAndRunResult)) {
|
||||
throw new WorkspaceMigrationBuilderExceptionV2(
|
||||
validateAndBuildResult,
|
||||
buildAndRunResult,
|
||||
'Multiple validation errors occurred while creating view filter',
|
||||
);
|
||||
}
|
||||
|
||||
this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatViewFilterMaps'],
|
||||
});
|
||||
|
||||
const { flatViewFilterMaps: recomputedExistingFlatViewFilterMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
|
||||
+5
@@ -92,6 +92,11 @@ export class CreateViewInput {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
calendarFieldMetadataId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
mainGroupByFieldMetadataId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ViewVisibility)
|
||||
@Field(() => ViewVisibility, {
|
||||
|
||||
+5
@@ -87,4 +87,9 @@ export class UpdateViewInput {
|
||||
@IsEnum(ViewVisibility)
|
||||
@Field(() => ViewVisibility, { nullable: true })
|
||||
visibility?: ViewVisibility;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
mainGroupByFieldMetadataId?: string;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ export class ViewDTO {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
kanbanAggregateOperationFieldMetadataId?: string | null;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
mainGroupByFieldMetadataId?: string | null;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
calendarFieldMetadataId?: string | null;
|
||||
|
||||
|
||||
@@ -138,6 +138,20 @@ export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
|
||||
@JoinColumn({ name: 'calendarFieldMetadataId' })
|
||||
calendarFieldMetadata: Relation<FieldMetadataEntity>;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
mainGroupByFieldMetadataId: string | null;
|
||||
|
||||
@ManyToOne(
|
||||
() => FieldMetadataEntity,
|
||||
(fieldMetadata) => fieldMetadata.mainGroupByFieldMetadataViews,
|
||||
{
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: 'mainGroupByFieldMetadataId' })
|
||||
mainGroupByFieldMetadata: Relation<FieldMetadataEntity>;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
|
||||
|
||||
+1
@@ -151,6 +151,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
relationTargetFieldMetadataId: null,
|
||||
relationTargetObjectMetadataId: null,
|
||||
morphId: null,
|
||||
|
||||
+1
@@ -94,6 +94,7 @@ describe('WorkspaceRepository', () => {
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFieldIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
};
|
||||
|
||||
mockInternalContext = {
|
||||
|
||||
+1
@@ -63,6 +63,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
|
||||
viewGroupIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
applicationId: null,
|
||||
}) as unknown as FlatFieldMetadata;
|
||||
|
||||
|
||||
+2
@@ -171,6 +171,7 @@ export const createCoreViews = async (
|
||||
icon,
|
||||
isCustom,
|
||||
openRecordIn,
|
||||
mainGroupByFieldMetadataId,
|
||||
kanbanAggregateOperation,
|
||||
kanbanAggregateOperationFieldMetadataId,
|
||||
applicationId,
|
||||
@@ -191,6 +192,7 @@ export const createCoreViews = async (
|
||||
: ViewOpenRecordIn.SIDE_PANEL,
|
||||
kanbanAggregateOperation,
|
||||
kanbanAggregateOperationFieldMetadataId,
|
||||
mainGroupByFieldMetadataId,
|
||||
workspaceId,
|
||||
anyFieldFilterValue: null,
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
|
||||
+1
-1
@@ -16,9 +16,9 @@ export interface ViewDefinition {
|
||||
icon?: string;
|
||||
isCustom?: boolean;
|
||||
openRecordIn?: ViewOpenRecordInType;
|
||||
kanbanFieldMetadataId?: string;
|
||||
kanbanAggregateOperation?: AggregateOperations;
|
||||
kanbanAggregateOperationFieldMetadataId?: string;
|
||||
mainGroupByFieldMetadataId?: string;
|
||||
calendarFieldMetadataId?: string;
|
||||
calendarLayout?: string;
|
||||
fields?: {
|
||||
|
||||
-1
@@ -42,7 +42,6 @@ export const calendarEventsAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconCalendar',
|
||||
kanbanFieldMetadataId: '',
|
||||
calendarFieldMetadataId:
|
||||
calendarEventObjectMetadata.fields.find(
|
||||
(field) =>
|
||||
|
||||
-1
@@ -43,7 +43,6 @@ export const companiesAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconList',
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -42,7 +42,6 @@ export const dashboardsAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconLayoutDashboard',
|
||||
kanbanFieldMetadataId: '',
|
||||
openRecordIn: ViewOpenRecordInType.RECORD_PAGE,
|
||||
filters: [],
|
||||
fields: [
|
||||
|
||||
-1
@@ -42,7 +42,6 @@ export const messageThreadsAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconList',
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -42,7 +42,6 @@ export const messagesAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconList',
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -41,7 +41,6 @@ export const notesAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconNotes',
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -39,7 +39,6 @@ export const opportunitiesAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconList',
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
+6
-4
@@ -29,6 +29,11 @@ export const opportunitiesByStageView = ({
|
||||
const viewUniversalIdentifier =
|
||||
STANDARD_OBJECTS.opportunity.views.byStage.universalIdentifier;
|
||||
|
||||
const stageFieldMetadataId =
|
||||
opportunityObjectMetadata.fields.find(
|
||||
(field) => field.standardId === OPPORTUNITY_STANDARD_FIELD_IDS.stage,
|
||||
)?.id ?? '';
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
universalIdentifier: viewUniversalIdentifier,
|
||||
@@ -39,16 +44,13 @@ export const opportunitiesByStageView = ({
|
||||
key: null,
|
||||
position: 2,
|
||||
icon: 'IconLayoutKanban',
|
||||
kanbanFieldMetadataId:
|
||||
opportunityObjectMetadata.fields.find(
|
||||
(field) => field.standardId === OPPORTUNITY_STANDARD_FIELD_IDS.stage,
|
||||
)?.id ?? '',
|
||||
kanbanAggregateOperation: AggregateOperations.MIN,
|
||||
kanbanAggregateOperationFieldMetadataId:
|
||||
opportunityObjectMetadata.fields.find(
|
||||
(field) => field.standardId === OPPORTUNITY_STANDARD_FIELD_IDS.amount,
|
||||
)?.id ?? '',
|
||||
filters: [],
|
||||
mainGroupByFieldMetadataId: stageFieldMetadataId,
|
||||
fields: [
|
||||
{
|
||||
fieldMetadataId:
|
||||
|
||||
-1
@@ -42,7 +42,6 @@ export const peopleAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconList',
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -41,7 +41,6 @@ export const tasksAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconList',
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [] /* [
|
||||
{
|
||||
fieldMetadataId:
|
||||
|
||||
+4
-1
@@ -42,7 +42,10 @@ export const tasksAssignedToMeView = ({
|
||||
key: null,
|
||||
position: 2,
|
||||
icon: 'IconUserCircle',
|
||||
kanbanFieldMetadataId: '',
|
||||
mainGroupByFieldMetadataId:
|
||||
taskObjectMetadata.fields.find(
|
||||
(field) => field.standardId === TASK_STANDARD_FIELD_IDS.status,
|
||||
)?.id ?? '',
|
||||
filters: [
|
||||
{
|
||||
fieldMetadataId:
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const tasksByStatusView = ({
|
||||
key: null,
|
||||
position: 1,
|
||||
icon: 'IconLayoutKanban',
|
||||
kanbanFieldMetadataId:
|
||||
mainGroupByFieldMetadataId:
|
||||
taskObjectMetadata.fields.find(
|
||||
(field) => field.standardId === TASK_STANDARD_FIELD_IDS.status,
|
||||
)?.id ?? '',
|
||||
|
||||
-1
@@ -44,7 +44,6 @@ export const timelineActivitiesAllView = ({
|
||||
position: 0,
|
||||
icon: 'IconList',
|
||||
openRecordIn: ViewOpenRecordInType.RECORD_PAGE,
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -40,7 +40,6 @@ export const workflowRunsAllView = ({
|
||||
position: 0,
|
||||
icon: 'IconHistoryToggle',
|
||||
openRecordIn: ViewOpenRecordInType.RECORD_PAGE,
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -46,7 +46,6 @@ export const workflowVersionsAllView = ({
|
||||
position: 0,
|
||||
icon: 'IconVersions',
|
||||
openRecordIn: ViewOpenRecordInType.RECORD_PAGE,
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -43,7 +43,6 @@ export const workflowsAllView = ({
|
||||
position: 0,
|
||||
icon: 'IconSettingsAutomation',
|
||||
openRecordIn: ViewOpenRecordInType.RECORD_PAGE,
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
-1
@@ -45,7 +45,6 @@ export const workspaceMembersAllView = ({
|
||||
key: 'INDEX',
|
||||
position: 0,
|
||||
icon: 'IconList',
|
||||
kanbanFieldMetadataId: '',
|
||||
filters: [],
|
||||
fields: [
|
||||
{
|
||||
|
||||
+4
@@ -21,6 +21,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a crea
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "flat field metadata label",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "flatFieldMetadataName",
|
||||
"objectMetadataId": "object-metadata-id-1",
|
||||
@@ -85,6 +86,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a dele
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "flat field metadata label",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "flatFieldMetadataName",
|
||||
"objectMetadataId": "object-metadata-id-1",
|
||||
@@ -167,6 +169,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "flat field metadata label",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "flatFieldMetadataName",
|
||||
"objectMetadataId": "object-metadata-id-1",
|
||||
@@ -213,6 +216,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "flat field metadata label",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "flatFieldMetadataName",
|
||||
"objectMetadataId": "object-metadata-id-1",
|
||||
|
||||
+1
@@ -416,6 +416,7 @@ export const VIEW_STANDARD_FIELD_IDS = {
|
||||
kanbanAggregateOperation: '20202020-8da2-45de-a731-61bed84b17a8',
|
||||
kanbanAggregateOperationFieldMetadataId:
|
||||
'20202020-b1b3-4bf3-85e4-dc7d58aa9b02',
|
||||
mainGroupByFieldMetadataId: '20202020-f782-4f89-96eb-083a02cca887',
|
||||
position: '20202020-e9db-4303-b271-e8250c450172',
|
||||
isCompact: '20202020-674e-4314-994d-05754ea7b22b',
|
||||
openRecordIn: '20202020-086d-4eef-9f03-56c6392eacb8',
|
||||
|
||||
@@ -21,6 +21,7 @@ export const getMockFieldMetadataEntity = <
|
||||
): FieldMetadataEntity => {
|
||||
return {
|
||||
calendarViews: [],
|
||||
mainGroupByFieldMetadataViews: [],
|
||||
viewFilters: [],
|
||||
viewFields: [],
|
||||
viewGroups: [],
|
||||
|
||||
Reference in New Issue
Block a user