diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts index 28c2455c3d7b..84cd0ff54248 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts @@ -73,6 +73,20 @@ export const useHandleRecordGroupField = ({ }) satisfies ViewGroup, ); + if ( + !existingGroupKeys.has(`${fieldMetadataItem.id}:`) && + fieldMetadataItem.isNullable === true + ) { + viewGroupsToCreate.push({ + __typename: 'ViewGroup', + id: v4(), + fieldValue: '', + isVisible: true, + position: fieldMetadataItem.options.length, + fieldMetadataId: fieldMetadataItem.id, + } satisfies ViewGroup); + } + const viewGroupsToDelete = view.viewGroups.filter( (group) => group.fieldMetadataId !== fieldMetadataItem.id, ); diff --git a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts index 73f4782479be..6a08a970ff66 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts @@ -1,4 +1,3 @@ -import { v4 } from 'uuid'; import { isDefined } from '~/utils/isDefined'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -42,46 +41,25 @@ export const mapViewGroupsToRecordGroupDefinitions = ({ (option) => option.value === viewGroup.fieldValue, ); - if (!selectedOption) { + if (!selectedOption && selectFieldMetadataItem.isNullable === false) { return null; } return { id: viewGroup.id, fieldMetadataId: viewGroup.fieldMetadataId, - type: RecordGroupDefinitionType.Value, - title: selectedOption.label, - value: selectedOption.value, - color: selectedOption.color, + type: !isDefined(selectedOption) + ? RecordGroupDefinitionType.NoValue + : RecordGroupDefinitionType.Value, + title: selectedOption?.label ?? 'No Value', + value: selectedOption?.value ?? null, + color: selectedOption?.color ?? 'transparent', position: viewGroup.position, isVisible: viewGroup.isVisible, } as RecordGroupDefinition; }) .filter(isDefined); - if (selectFieldMetadataItem.isNullable === true) { - const viewGroup = viewGroups.find( - (viewGroup) => viewGroup.fieldValue === '', - ); - - const noValueColumn = { - id: viewGroup?.id ?? v4(), - title: 'No Value', - type: RecordGroupDefinitionType.NoValue, - value: null, - position: - viewGroup?.position ?? - recordGroupDefinitionsFromViewGroups - .map((option) => option.position) - .reduce((a, b) => Math.max(a, b), 0) + 1, - isVisible: viewGroup?.isVisible ?? true, - fieldMetadataId: selectFieldMetadataItem.id, - color: 'transparent', - } satisfies RecordGroupDefinition; - - return [...recordGroupDefinitionsFromViewGroups, noValueColumn]; - } - return recordGroupDefinitionsFromViewGroups.sort( (a, b) => a.position - b.position, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts index 61757b18ebfe..89a2dfa1af71 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts @@ -8,6 +8,7 @@ import { } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; @@ -28,7 +29,7 @@ export class FieldMetadataRelatedRecordsService { oldFieldMetadata: FieldMetadataEntity, newFieldMetadata: FieldMetadataEntity, transactionManager?: EntityManager, - ) { + ): Promise { if ( !isSelectFieldMetadataType(newFieldMetadata.type) || !isSelectFieldMetadataType(oldFieldMetadata.type) @@ -54,10 +55,7 @@ export class FieldMetadataRelatedRecordsService { continue; } - const maxPosition = view.viewGroups.reduce( - (max, viewGroup) => Math.max(max, viewGroup.position), - 0, - ); + const maxPosition = this.getMaxPosition(view.viewGroups); const viewGroupsToCreate = created.map((option, index) => viewGroupRepository.create({ @@ -72,21 +70,19 @@ export class FieldMetadataRelatedRecordsService { await viewGroupRepository.insert(viewGroupsToCreate, transactionManager); for (const { old: oldOption, new: newOption } of updated) { - const viewGroup = view.viewGroups.find( - (viewGroup) => viewGroup.fieldValue === oldOption.value, + const existingViewGroup = view.viewGroups.find( + (group) => group.fieldValue === oldOption.value, ); - if (!viewGroup) { - throw new Error(`View group not found for option ${oldOption.value}`); + if (!existingViewGroup) { + throw new Error( + `View group not found for option "${oldOption.value}" during update.`, + ); } await viewGroupRepository.update( - { - id: viewGroup.id, - }, - { - fieldValue: newOption.value, - }, + { id: existingViewGroup.id }, + { fieldValue: newOption.value }, transactionManager, ); } @@ -100,13 +96,49 @@ export class FieldMetadataRelatedRecordsService { }, transactionManager, ); + + await this.syncNoValueViewGroup( + newFieldMetadata, + view, + viewGroupRepository, + transactionManager, + ); + } + } + + private async syncNoValueViewGroup( + fieldMetadata: FieldMetadataEntity, + view: ViewWorkspaceEntity, + viewGroupRepository: WorkspaceRepository, + transactionManager?: EntityManager, + ): Promise { + const noValueGroup = view.viewGroups.find( + (group) => group.fieldValue === '', + ); + + if (fieldMetadata.isNullable && !noValueGroup) { + const maxPosition = this.getMaxPosition(view.viewGroups); + const newGroup = viewGroupRepository.create({ + fieldMetadataId: fieldMetadata.id, + fieldValue: '', + position: maxPosition + 1, + isVisible: true, + viewId: view.id, + }); + + await viewGroupRepository.insert(newGroup, transactionManager); + } else if (!fieldMetadata.isNullable && noValueGroup) { + await viewGroupRepository.delete( + { id: noValueGroup.id }, + transactionManager, + ); } } private getOptionsDifferences( oldOptions: (FieldMetadataDefaultOption | FieldMetadataComplexOption)[], newOptions: (FieldMetadataDefaultOption | FieldMetadataComplexOption)[], - ) { + ): Differences { const differences: Differences< FieldMetadataDefaultOption | FieldMetadataComplexOption > = { @@ -115,12 +147,8 @@ export class FieldMetadataRelatedRecordsService { deleted: [], }; - const oldOptionsMap = new Map( - oldOptions.map((option) => [option.id, option]), - ); - const newOptionsMap = new Map( - newOptions.map((option) => [option.id, option]), - ); + const oldOptionsMap = new Map(oldOptions.map((opt) => [opt.id, opt])); + const newOptionsMap = new Map(newOptions.map((opt) => [opt.id, opt])); for (const newOption of newOptions) { const oldOption = oldOptionsMap.get(newOption.id); @@ -150,7 +178,7 @@ export class FieldMetadataRelatedRecordsService { 'view', ); - return await viewRepository.find({ + return viewRepository.find({ where: { viewGroups: { fieldMetadataId: fieldMetadata.id, @@ -159,4 +187,8 @@ export class FieldMetadataRelatedRecordsService { relations: ['viewGroups'], }); } + + private getMaxPosition(viewGroups: ViewGroupWorkspaceEntity[]): number { + return viewGroups.reduce((max, group) => Math.max(max, group.position), 0); + } }