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
@@ -20,6 +20,9 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
return {
...fieldMetadataWithoutRelations,
kanbanAggregateOperationViewIds:
fieldMetadataEntity.kanbanAggregateOperationViews.map(({ id }) => id),
calendarViewIds: fieldMetadataEntity.calendarViews.map(({ id }) => id),
viewGroupIds: fieldMetadataEntity.viewGroups.map(({ id }) => id),
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
@@ -19,6 +19,7 @@ import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
import {
FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS,
type FlatFieldMetadataUpdateSideEffects,
handleFlatFieldMetadataUpdateSideEffect,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util';
@@ -34,6 +35,8 @@ type FromUpdateFieldInputToFlatFieldMetadataArgs = {
| 'flatFieldMetadataMaps'
| 'flatViewFilterMaps'
| 'flatViewGroupMaps'
| 'flatViewMaps'
| 'flatViewFieldMaps'
>;
type FlatFieldMetadataAndIndexToUpdate = {
@@ -46,6 +49,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
updateFieldInput: rawUpdateFieldInput,
flatViewFilterMaps,
flatViewGroupMaps,
flatViewMaps,
flatViewFieldMaps,
}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult<FlatFieldMetadataAndIndexToUpdate> => {
const updateFieldInputInformalProperties =
extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [
@@ -114,6 +119,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
return {
status: 'success',
result: {
flatViewsToUpdate: [],
flatViewsToDelete: [],
flatViewGroupsToCreate: [],
flatViewGroupsToDelete: [],
flatViewGroupsToUpdate: [],
@@ -123,6 +130,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatIndexMetadatasToCreate: [],
flatViewFiltersToDelete: [],
flatViewFiltersToUpdate: [],
flatViewFieldsToDelete: [],
},
};
}
@@ -152,15 +160,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
];
const initialAccumulator: FlatFieldMetadataAndIndexToUpdate = {
...structuredClone(FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS),
flatFieldMetadatasToUpdate: [],
flatIndexMetadatasToUpdate: [],
flatViewFiltersToDelete: [],
flatViewFiltersToUpdate: [],
flatViewGroupsToCreate: [],
flatViewGroupsToDelete: [],
flatIndexMetadatasToCreate: [],
flatIndexMetadatasToDelete: [],
flatViewGroupsToUpdate: [],
};
updatedEditableFieldProperties.options = !isDefined(
@@ -190,6 +191,9 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatViewFiltersToUpdate,
flatIndexMetadatasToCreate,
flatIndexMetadatasToDelete,
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewsToUpdate,
} = handleFlatFieldMetadataUpdateSideEffect({
flatViewFilterMaps,
flatViewGroupMaps,
@@ -198,6 +202,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatFieldMetadataMaps,
flatIndexMaps,
toFlatFieldMetadata,
flatViewMaps,
flatViewFieldMaps,
});
return {
@@ -237,6 +243,18 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
...accumulator.flatIndexMetadatasToCreate,
...flatIndexMetadatasToCreate,
],
flatViewsToDelete: [
...accumulator.flatViewsToDelete,
...flatViewsToDelete,
],
flatViewFieldsToDelete: [
...accumulator.flatViewFieldsToDelete,
...flatViewFieldsToDelete,
],
flatViewsToUpdate: [
...accumulator.flatViewsToUpdate,
...flatViewsToUpdate,
],
};
},
initialAccumulator,
@@ -23,6 +23,7 @@ export const getDefaultFlatFieldMetadata = ({
const createdAt = new Date();
return {
calendarViewIds: [],
viewFieldIds: [],
description: createFieldInput.description ?? null,
id: fieldMetadataId,
@@ -57,5 +58,6 @@ export const getDefaultFlatFieldMetadata = ({
applicationId: null,
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
} as const satisfies FlatFieldMetadata;
};
@@ -2,7 +2,6 @@ import { type EnumFieldMetadataType, type FromTo } from 'twenty-shared/types';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import {
type FlatViewFiltersToDeleteAndUpdate,
recomputeViewFiltersOnFlatFieldMetadataOptionsUpdate,
@@ -15,7 +14,7 @@ import {
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type';
type HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs = FromTo<
FlatFieldMetadata,
FlatFieldMetadata<EnumFieldMetadataType>,
'flatFieldMetadata'
> &
Pick<AllFlatEntityMaps, 'flatViewFilterMaps' | 'flatViewGroupMaps'>;
@@ -42,13 +41,6 @@ export const handleEnumFlatFieldMetadataUpdateSideEffects = ({
EMPTY_ENUM_FIELD_METADATA_SIDE_EFFECT_RESULT,
);
if (
!isEnumFlatFieldMetadata(toFlatFieldMetadata) ||
!isEnumFlatFieldMetadata(fromFlatFieldMetadata)
) {
return sideEffectResult;
}
if (
JSON.stringify(fromFlatFieldMetadata.options) !==
JSON.stringify(toFlatFieldMetadata.options)
@@ -0,0 +1,90 @@
import { type FromTo } from 'twenty-shared/types';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { reduceFlatViewGroupsByViewId } from 'src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
type HandleFlatFieldMetadataDeactivationSideEffectsArgs = FromTo<
FlatFieldMetadata,
'flatFieldMetadata'
> &
Pick<
AllFlatEntityMaps,
| 'flatViewMaps'
| 'flatViewFilterMaps'
| 'flatViewFieldMaps'
| 'flatViewGroupMaps'
>;
export type FieldMetadataDeactivationSideEffect = {
flatViewsToDelete: FlatView[];
flatViewsToUpdate: FlatView[];
flatViewFieldsToDelete: FlatViewField[];
flatViewFiltersToDelete: FlatViewFilter[];
};
export const handleFieldMetadataDeactivationSideEffects = ({
flatViewMaps,
fromFlatFieldMetadata,
flatViewFieldMaps,
flatViewFilterMaps,
flatViewGroupMaps,
}: HandleFlatFieldMetadataDeactivationSideEffectsArgs): FieldMetadataDeactivationSideEffect => {
const flatViewFiltersToDelete = findManyFlatEntityByIdInFlatEntityMapsOrThrow(
{
flatEntityIds: fromFlatFieldMetadata.viewFilterIds,
flatEntityMaps: flatViewFilterMaps,
},
);
const flatViewFieldsToDelete = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: fromFlatFieldMetadata.viewFieldIds,
flatEntityMaps: flatViewFieldMaps,
});
const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: fromFlatFieldMetadata.viewGroupIds,
flatEntityMaps: flatViewGroupMaps,
});
const { flatViewGroupRecordByViewId } = reduceFlatViewGroupsByViewId({
flatViewGroups,
});
// Note: We assume a view only has view groups related to one field
const viewIdsToDelete = [
...new Set([
...Object.keys(flatViewGroupRecordByViewId),
...fromFlatFieldMetadata.calendarViewIds,
]),
];
const flatViewsToDelete = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: viewIdsToDelete,
flatEntityMaps: flatViewMaps,
});
const viewIdsToUpdate =
fromFlatFieldMetadata.kanbanAggregateOperationViewIds.filter(
(viewId) => !viewIdsToDelete.includes(viewId),
);
const flatViewsToUpdate = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: viewIdsToUpdate,
flatEntityMaps: flatViewMaps,
}).map((flatView) => ({
...flatView,
kanbanAggregateOperation: null,
kanbanAggregateOperationFieldMetadataId: null,
}));
return {
flatViewsToUpdate,
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewFiltersToDelete,
};
};
@@ -3,17 +3,23 @@ import { type FromTo } from 'twenty-shared/types';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { handleEnumFlatFieldMetadataUpdateSideEffects } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util';
import {
type FieldMetadataDeactivationSideEffect,
handleFieldMetadataDeactivationSideEffects,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-field-metadata-deactivation-side-effects.util';
import {
type FieldMetadataUpdateIndexSideEffect,
handleIndexChangesDuringFieldUpdate,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { type FlatViewFiltersToDeleteAndUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util';
import { type FlatViewGroupsToDeleteUpdateAndCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util';
export type FlatFieldMetadataUpdateSideEffects =
FlatViewFiltersToDeleteAndUpdate &
FlatViewGroupsToDeleteUpdateAndCreate &
FieldMetadataUpdateIndexSideEffect;
FieldMetadataUpdateIndexSideEffect &
FieldMetadataDeactivationSideEffect;
type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
FlatFieldMetadata,
@@ -26,8 +32,25 @@ type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
| 'flatFieldMetadataMaps'
| 'flatViewFilterMaps'
| 'flatViewGroupMaps'
| 'flatViewMaps'
| 'flatViewFieldMaps'
>;
export const FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS: FlatFieldMetadataUpdateSideEffects =
{
flatIndexMetadatasToUpdate: [],
flatViewFiltersToDelete: [],
flatViewFiltersToUpdate: [],
flatViewGroupsToCreate: [],
flatViewGroupsToDelete: [],
flatIndexMetadatasToDelete: [],
flatIndexMetadatasToCreate: [],
flatViewGroupsToUpdate: [],
flatViewsToDelete: [],
flatViewFieldsToDelete: [],
flatViewsToUpdate: [],
};
export const handleFlatFieldMetadataUpdateSideEffect = ({
fromFlatFieldMetadata,
toFlatFieldMetadata,
@@ -36,19 +59,59 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
flatFieldMetadataMaps,
flatViewFilterMaps,
flatViewGroupMaps,
flatViewMaps,
flatViewFieldMaps,
}: HandleFlatFieldMetadataUpdateSideEffectArgs): FlatFieldMetadataUpdateSideEffects => {
const {
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatViewGroupsToCreate,
flatViewGroupsToDelete,
flatViewGroupsToUpdate,
} = handleEnumFlatFieldMetadataUpdateSideEffects({
flatViewFilterMaps,
flatViewGroupMaps,
fromFlatFieldMetadata,
toFlatFieldMetadata,
});
const sideEffectResult = structuredClone(
FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS,
);
const isDeactivation =
fromFlatFieldMetadata.isActive === true &&
toFlatFieldMetadata.isActive === false;
if (isDeactivation) {
const {
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewFiltersToDelete,
flatViewsToUpdate,
} = handleFieldMetadataDeactivationSideEffects({
flatViewMaps,
fromFlatFieldMetadata,
toFlatFieldMetadata,
flatViewFieldMaps,
flatViewFilterMaps,
flatViewGroupMaps,
});
sideEffectResult.flatViewsToUpdate.push(...flatViewsToUpdate);
sideEffectResult.flatViewsToDelete.push(...flatViewsToDelete);
sideEffectResult.flatViewFieldsToDelete.push(...flatViewFieldsToDelete);
sideEffectResult.flatViewFiltersToDelete.push(...flatViewFiltersToDelete);
} else if (
isEnumFlatFieldMetadata(toFlatFieldMetadata) &&
isEnumFlatFieldMetadata(fromFlatFieldMetadata)
) {
const {
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatViewGroupsToCreate,
flatViewGroupsToDelete,
flatViewGroupsToUpdate,
} = handleEnumFlatFieldMetadataUpdateSideEffects({
flatViewFilterMaps,
flatViewGroupMaps,
fromFlatFieldMetadata,
toFlatFieldMetadata,
});
sideEffectResult.flatViewFiltersToUpdate.push(...flatViewFiltersToUpdate);
sideEffectResult.flatViewGroupsToCreate.push(...flatViewGroupsToCreate);
sideEffectResult.flatViewGroupsToDelete.push(...flatViewGroupsToDelete);
sideEffectResult.flatViewGroupsToUpdate.push(...flatViewGroupsToUpdate);
sideEffectResult.flatViewFiltersToDelete.push(...flatViewFiltersToDelete);
}
const {
flatIndexMetadatasToUpdate,
@@ -62,14 +125,15 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
flatFieldMetadataMaps,
});
return {
flatIndexMetadatasToUpdate,
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatViewGroupsToCreate,
flatViewGroupsToDelete,
flatIndexMetadatasToDelete,
flatIndexMetadatasToCreate,
flatViewGroupsToUpdate,
};
sideEffectResult.flatIndexMetadatasToUpdate.push(
...flatIndexMetadatasToUpdate,
);
sideEffectResult.flatIndexMetadatasToCreate.push(
...flatIndexMetadatasToCreate,
);
sideEffectResult.flatIndexMetadatasToDelete.push(
...flatIndexMetadatasToDelete,
);
return sideEffectResult;
};