From 347a084f2c21b8cd35de076d6d3b0254cc7ad90b Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Sat, 7 Mar 2026 06:03:51 +0000 Subject: [PATCH] Orphaned view sorts crash People page after field deactivation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/3162?type=bug When a field is deactivated/deleted, the server-side cleanup omits view sorts referencing that field, leaving orphaned view sorts in the database. The frontend's `EditableSortChip` throws an unhandled exception when trying to render these orphaned sorts, crashing the entire page. Fix: ## Changes ### Server-side: Add view sort cleanup to field deactivation side effects **`handle-field-metadata-deactivation-side-effects.util.ts`** — Added `flatViewSortMaps` to the function's input args (via `Pick`), added `FlatViewSort` import, added `flatViewSortsToDelete` to the return type and implementation. Uses `fromFlatFieldMetadata.viewSortIds` (which was already populated but never consumed) to find and return the view sorts to delete. **`handle-flat-field-metadata-update-side-effect.util.ts`** — Added `flatViewSortMaps` to args, `flatViewSortsToDelete: []` to empty side effects constant, and propagation of `flatViewSortsToDelete` from the deactivation handler into the result. **`from-update-field-input-to-flat-field-metadata.util.ts`** — Added `flatViewSortMaps` to the args type, destructured it, passed it through to `handleFlatFieldMetadataUpdateSideEffect`, and accumulated `flatViewSortsToDelete` in the reduce result. **`field-metadata.service.ts`** — Added `flatViewSortMaps` to the cache fetch keys and destructuring, passed it to `fromUpdateFieldInputToFlatFieldMetadata`, and added a `viewSort` entry in `allFlatEntityOperationByMetadataName` with `flatEntityToDelete: flatViewSortsToDelete`. ### Frontend: Defense-in-depth filter for orphaned sorts **`ViewBarRecordSortEffect.tsx`** — Before setting `currentRecordSorts`, filters `currentView.viewSorts` to only include sorts whose `fieldMetadataId` exists in `objectMetadataItem.fields`. This prevents the crash for any orphaned sorts that already exist in the database (before the server-side fix takes effect for new deactivations). --- .../views/components/ViewBarRecordSortEffect.tsx | 7 ++++++- .../field-metadata/services/field-metadata.service.ts | 9 +++++++++ ...m-update-field-input-to-flat-field-metadata.util.ts | 8 ++++++++ ...le-field-metadata-deactivation-side-effects.util.ts | 10 ++++++++++ ...ndle-flat-field-metadata-update-side-effect.util.ts | 6 ++++++ 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/views/components/ViewBarRecordSortEffect.tsx b/packages/twenty-front/src/modules/views/components/ViewBarRecordSortEffect.tsx index b83a057fc5c..d0d135712e4 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarRecordSortEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarRecordSortEffect.tsx @@ -46,7 +46,12 @@ export const ViewBarRecordSortEffect = () => { } if (isDefined(currentView)) { - setCurrentRecordSorts(currentView.viewSorts); + const validViewSorts = currentView.viewSorts.filter((viewSort) => + objectMetadataItem.fields.some( + (field) => field.id === viewSort.fieldMetadataId, + ), + ); + setCurrentRecordSorts(validViewSorts); setHasInitializedCurrentRecordSorts(true); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index 2ed425d1d2b..c24116873e6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -181,6 +181,7 @@ export class FieldMetadataService extends TypeOrmQueryService; type FlatFieldMetadataAndIndexToUpdate = { @@ -51,6 +52,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ flatViewGroupMaps, flatViewMaps, flatViewFieldMaps, + flatViewSortMaps, isSystemBuild, }: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult => { const updateFieldInputInformalProperties = @@ -150,6 +152,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ toFlatFieldMetadata, flatViewMaps, flatViewFieldMaps, + flatViewSortMaps, flatApplication, }); @@ -171,6 +174,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ flatIndexMetadatasToDelete, flatViewsToDelete, flatViewFieldsToDelete, + flatViewSortsToDelete, flatViewsToUpdate, flatFieldMetadatasToUpdate: flatFieldMetadatasToUpdateFromSideEffect, } = sideEffectResult.result; @@ -222,6 +226,10 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ ...accumulator.flatViewFieldsToDelete, ...flatViewFieldsToDelete, ], + flatViewSortsToDelete: [ + ...accumulator.flatViewSortsToDelete, + ...flatViewSortsToDelete, + ], flatViewsToUpdate: [ ...accumulator.flatViewsToUpdate, ...flatViewsToUpdate, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-field-metadata-deactivation-side-effects.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-field-metadata-deactivation-side-effects.util.ts index 3b3222702ca..4ace18c4bbe 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-field-metadata-deactivation-side-effects.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-field-metadata-deactivation-side-effects.util.ts @@ -6,6 +6,7 @@ import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow 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 { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type'; import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; type HandleFlatFieldMetadataDeactivationSideEffectsArgs = FromTo< @@ -18,6 +19,7 @@ type HandleFlatFieldMetadataDeactivationSideEffectsArgs = FromTo< | 'flatViewFilterMaps' | 'flatViewFieldMaps' | 'flatViewGroupMaps' + | 'flatViewSortMaps' >; export type FieldMetadataDeactivationSideEffect = { @@ -25,6 +27,7 @@ export type FieldMetadataDeactivationSideEffect = { flatViewsToUpdate: FlatView[]; flatViewFieldsToDelete: FlatViewField[]; flatViewFiltersToDelete: FlatViewFilter[]; + flatViewSortsToDelete: FlatViewSort[]; }; export const handleFieldMetadataDeactivationSideEffects = ({ @@ -33,6 +36,7 @@ export const handleFieldMetadataDeactivationSideEffects = ({ flatViewFieldMaps, flatViewFilterMaps, flatViewGroupMaps, + flatViewSortMaps, }: HandleFlatFieldMetadataDeactivationSideEffectsArgs): FieldMetadataDeactivationSideEffect => { const flatViewFiltersToDelete = findManyFlatEntityByIdInFlatEntityMapsOrThrow( { @@ -46,6 +50,11 @@ export const handleFieldMetadataDeactivationSideEffects = ({ flatEntityMaps: flatViewFieldMaps, }); + const flatViewSortsToDelete = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: fromFlatFieldMetadata.viewSortIds, + flatEntityMaps: flatViewSortMaps, + }); + const flatViewsAffected: FlatView[] = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityIds: fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds, @@ -104,5 +113,6 @@ export const handleFieldMetadataDeactivationSideEffects = ({ flatViewsToDelete, flatViewFieldsToDelete, flatViewFiltersToDelete, + flatViewSortsToDelete, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts index 6fde0d0618e..8c2b059c4da 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts @@ -41,6 +41,7 @@ type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo< | 'flatViewGroupMaps' | 'flatViewMaps' | 'flatViewFieldMaps' + | 'flatViewSortMaps' > & { flatApplication: FlatApplication; }; @@ -57,6 +58,7 @@ export const FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS: FlatFieldMetadataUpd flatViewGroupsToUpdate: [], flatViewsToDelete: [], flatViewFieldsToDelete: [], + flatViewSortsToDelete: [], flatViewsToUpdate: [], flatFieldMetadatasToUpdate: [], }; @@ -71,6 +73,7 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({ flatViewGroupMaps, flatViewMaps, flatViewFieldMaps, + flatViewSortMaps, flatApplication, }: HandleFlatFieldMetadataUpdateSideEffectArgs): FieldInputTranspilationResult => { const sideEffectResult = structuredClone( @@ -86,6 +89,7 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({ flatViewsToDelete, flatViewFieldsToDelete, flatViewFiltersToDelete, + flatViewSortsToDelete, flatViewsToUpdate, } = handleFieldMetadataDeactivationSideEffects({ flatViewMaps, @@ -94,12 +98,14 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({ flatViewFieldMaps, flatViewFilterMaps, flatViewGroupMaps, + flatViewSortMaps, }); sideEffectResult.flatViewsToUpdate.push(...flatViewsToUpdate); sideEffectResult.flatViewsToDelete.push(...flatViewsToDelete); sideEffectResult.flatViewFieldsToDelete.push(...flatViewFieldsToDelete); sideEffectResult.flatViewFiltersToDelete.push(...flatViewFiltersToDelete); + sideEffectResult.flatViewSortsToDelete.push(...flatViewSortsToDelete); } else if ( isEnumFlatFieldMetadata(toFlatFieldMetadata) && isEnumFlatFieldMetadata(fromFlatFieldMetadata)