Field deactivation side effect views calendar kanban viewFields (#15180)

# Introduction
Handling both:
- field deactivation side effect on view fields, view filters and views
- field deactivation side effect on view that targets it as
`kanbanAggregateFieldMetadataId`
- field deactivation side effect on view that targets it as
`calendarFieldMetadataId`

## Coverage
added coverage
```ts
 PASS  test/integration/metadata/suites/field-metadata/kanban-aggregate-field-deactivation-deletes-views.integration-spec.ts (13.132 s)
  kanban-aggregate-field-deactivation-nullifies-kanban-properties
    ✓ should nullify kanban properties when field used as kanbanAggregateOperationFieldMetadataId is deactivated (3923 ms)
    ✓ should not modify views when field not used as kanbanAggregateOperationFieldMetadataId is deactivated (2958 ms)
    ✓ should nullify kanban properties on multiple views when they all use the same field as kanbanAggregateOperationFieldMetadataId (2542 ms)
    ✓ should nullify kanban properties when views have different aggregate operations on same field (3380 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        13.154 s
```

```ts
 PASS  test/integration/metadata/suites/field-metadata/view-group-field-deactivation-deletes-views.integration-spec.ts (12.639 s)
  view-group-field-deactivation-deletes-views
    ✓ should delete view when field used in view group is deactivated (3469 ms)
    ✓ should not delete view when field not used in view group is deactivated (3109 ms)
    ✓ should delete multiple views when they all use the same field in view groups (2741 ms)
    ✓ should handle deactivation when view has multiple view groups with different fields (3008 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        12.664 s
```

```ts
 PASS  test/integration/metadata/suites/field-metadata/calendar-field-deactivation-deletes-views.integration-spec.ts (14.579 s)
  calendar-field-deactivation-deletes-views
    ✓ should delete view when field used as calendarFieldMetadataId is deactivated (3388 ms)
    ✓ should not delete view when field not used as calendarFieldMetadataId is deactivated (2438 ms)
    ✓ should delete multiple views when they all use the same field as calendarFieldMetadataId (2635 ms)
    ✓ should handle deactivation when views have different calendar layouts on same field (3195 ms)
    ✓ should delete calendar view but not other view types when calendar field is deactivated (2682 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        14.601 s, estimated 15 s
```

## View soft deletion
We decided to remove the soft deletion grain on all the views, in this
PR context we've only removed soft deleted validation requirement on any
view entities

## Conclusion

close https://github.com/twentyhq/core-team-issues/issues/1754
This commit is contained in:
Paul Rastoin
2025-10-21 16:12:03 +02:00
committed by GitHub
parent 72fd8ae8d2
commit 45473218d3
45 changed files with 1674 additions and 194 deletions
@@ -17,6 +17,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/inte
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
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 { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
@@ -101,6 +102,17 @@ export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
@Column({ nullable: true, type: 'uuid' })
kanbanAggregateOperationFieldMetadataId: string | null;
@ManyToOne(
() => FieldMetadataEntity,
(FieldMetadataEntity) => FieldMetadataEntity.kanbanAggregateOperationViews,
{
onDelete: 'CASCADE',
nullable: true,
},
)
@JoinColumn({ name: 'kanbanAggregateOperationFieldMetadataId' })
kanbanAggregateOperationFieldMetadata: Relation<FieldMetadataEntity>;
@Column({
type: 'enum',
enum: Object.values(ViewCalendarLayout),
@@ -112,6 +124,17 @@ export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
@Column({ nullable: true, type: 'uuid' })
calendarFieldMetadataId: string | null;
@ManyToOne(
() => FieldMetadataEntity,
(fieldMetadata) => fieldMetadata.calendarViews,
{
onDelete: 'CASCADE',
nullable: true,
},
)
@JoinColumn({ name: 'calendarFieldMetadataId' })
calendarFieldMetadata: Relation<FieldMetadataEntity>;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@@ -31,13 +31,20 @@ export class ViewV2Service {
createViewInput: CreateViewInput;
workspaceId: string;
}): Promise<ViewDTO> {
const { flatObjectMetadataMaps, flatViewMaps: existingFlatViewMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatViewMaps'],
},
);
const {
flatObjectMetadataMaps,
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatViewMaps',
'flatFieldMetadataMaps',
],
},
);
const flatViewFromCreateInput = fromCreateViewInputToFlatViewToCreate({
createViewInput,
@@ -57,6 +64,7 @@ export class ViewV2Service {
},
dependencyAllFlatEntityMaps: {
flatObjectMetadataMaps: flatObjectMetadataMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
@@ -93,11 +101,14 @@ export class ViewV2Service {
updateViewInput: UpdateViewInput;
workspaceId: string;
}): Promise<ViewDTO> {
const { flatViewMaps: existingFlatViewMaps } =
const {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
flatMapsKeys: ['flatViewMaps', 'flatFieldMetadataMaps'],
},
);
@@ -118,6 +129,9 @@ export class ViewV2Service {
flatEntityToUpdate: [flatViewFromUpdateInput],
}),
},
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
},
@@ -153,11 +167,14 @@ export class ViewV2Service {
deleteViewInput: DeleteViewInput;
workspaceId: string;
}): Promise<ViewDTO> {
const { flatViewMaps: existingFlatViewMaps } =
const {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
flatMapsKeys: ['flatViewMaps', 'flatFieldMetadataMaps'],
},
);
@@ -178,6 +195,9 @@ export class ViewV2Service {
flatEntityToUpdate: [optimisticallyUpdatedFlatViewWithDeletedAt],
}),
},
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
},
@@ -213,11 +233,14 @@ export class ViewV2Service {
destroyViewInput: DestroyViewInput;
workspaceId: string;
}): Promise<ViewDTO> {
const { flatViewMaps: existingFlatViewMaps } =
const {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
flatMapsKeys: ['flatViewMaps', 'flatFieldMetadataMaps'],
},
);
@@ -243,6 +266,9 @@ export class ViewV2Service {
view: true,
},
},
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
workspaceId,
},
);