From 7ce22d5c7e20ca4cc1e86da8f7553e4d7ce7bf9a Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:00:03 +0100 Subject: [PATCH] breaking (soft) - Migrate viewGroup.fieldMetadataId -> view.mainGroupByFieldMetadataId (2/3) (#16277) Should be merged once https://github.com/twentyhq/twenty/pull/16206 has been released + command run to prod In this PR - Remove usage of viewGroup.fieldMetadataId, both in BE and FE states. - But we still need to properly populate it until we fully remove viewGroup.fieldMetadataId from db and ORM entity (upcoming 3rd PR out of 3). fieldMetadataId was removed from CoreViewGroup type and CreateViewGroupInput and is determined BE-side based on the associated view's mainGroupByFieldMetadataId. **I expect this means a downtime on viewGroup creation, until both FE and BE are deployed and cache is flushed.** This seems acceptable to me as it only regards viewGroup creation. - this information is replaced by view.mainGroupByFieldMetadataID - Handle view group creation, update and deletion in the BE as a side-effect of a view creation, update or deletion. Optimistic effects are still used - Add validation at view creation or update regarding mainGroupByFieldMetadata Left to do in 3rd PR - Remove viewGroup.fieldMetadataId from db and ORM entity - Restore feature allowing to update an existing grouped view's group by field (already OK on BE side but need to rebuild FE optimistic) --- .../src/generated-metadata/graphql.ts | 32 +- .../twenty-front/src/generated/graphql.ts | 32 +- .../useSetViewTypeFromLayoutOptionsMenu.ts | 12 +- .../useProcessTableWithGroupRecordDrop.ts | 19 +- .../hooks/useRecordGroupFilter.ts | 12 +- .../hooks/useReorderRecordGroups.ts | 20 +- .../record-group/hooks/useSetRecordGroups.ts | 45 ++- .../types/RecordGroupDefinition.ts | 1 - .../hooks/useHandleRecordGroupField.ts | 75 ++--- .../hooks/useLoadRecordIndexStates.ts | 13 +- .../RecordTableRecordGroupSectionAddNew.tsx | 8 +- .../views/graphql/fragments/viewFragment.ts | 2 +- .../graphql/fragments/viewGroupFragment.ts | 1 - .../hooks/internal/usePersistViewGroup.ts | 8 +- .../hooks/useCreateViewFromCurrentView.ts | 62 ++-- .../views/hooks/useGetViewGroupsFilters.ts | 6 +- .../views/hooks/useRefreshAllCoreViews.ts | 6 +- .../useRefreshCoreViewsByObjectMetadataId.ts | 10 +- .../views/hooks/useSaveCurrentViewGroups.ts | 28 +- .../useTriggerViewGroupOptimisticEffect.ts | 1 + .../src/modules/views/types/ViewGroup.ts | 1 - .../utils/convertCoreViewGroupToViewGroup.ts | 3 +- .../mapViewGroupsToRecordGroupDefinitions.ts | 11 +- .../utils/recordGroupDefinitionToViewGroup.ts | 1 - .../src/testing/mock-data/views.ts | 4 + .../twenty-config/config-variables.ts | 2 +- ...metadata-many-to-one-relations.constant.ts | 4 - ...ity-maps-through-mutation-or-throw.spec.ts | 10 +- ...ity-maps-through-mutation-or-throw.spec.ts | 2 +- .../__mocks__/get-flat-field-metadata.mock.ts | 1 - ...elation-target-flat-field-metadata-mock.ts | 1 - ...lat-field-metadatas-to-create.spec.ts.snap | 4 - ...e-flat-field-metadata-map-cache.service.ts | 64 ++-- .../types/flat-field-metadata.type.ts | 7 +- ...data-entity-to-flat-field-metadata.util.ts | 1 - ...d-metadata-from-create-field-input.util.ts | 1 - ...field-metadata-update-side-effects.util.ts | 8 +- ...metadata-deactivation-side-effects.util.ts | 11 +- ...-field-metadata-update-side-effect.util.ts | 1 + ...-field-metadata-is-nullable-update.util.ts | 14 +- ...flat-field-metadata-options-update.util.ts | 15 +- ...oup-entity-relation-properties.constant.ts | 1 - ...te-flat-view-groups-on-view-create.util.ts | 76 +++++ ...input-to-flat-view-group-to-create.util.ts | 8 +- ...-view-input-to-flat-view-to-create.util.ts | 32 +- ...ut-to-flat-view-to-update-or-throw.util.ts | 45 ++- ...andle-flat-view-update-side-effect.util.ts | 59 ++++ .../object-metadata.service.ts | 9 +- ...-field-metadatas-for-custom-object.util.ts | 20 +- ...-field-metadatas-for-custom-object.util.ts | 2 - .../dtos/inputs/create-view-group.input.ts | 4 - .../view-group/dtos/view-group.dto.ts | 3 - .../exceptions/view-group.exception.ts | 1 + .../resolvers/view-group.resolver.ts | 4 +- .../view-group/services/view-group.service.ts | 19 +- ...roup-graphql-api-exception-handler.util.ts | 26 ++ ...view-group-graphql-api-exception.filter.ts | 32 ++ .../view-group/view-group.module.ts | 2 + .../view/dtos/inputs/update-view.input.ts | 2 +- .../view/services/view.service.ts | 79 ++--- ...view-graphql-api-exception-handler.util.ts | 5 + .../workspace-entity-manager.spec.ts | 1 - .../__tests__/workspace.repository.spec.ts | 1 - ...reate-standard-field-flat-metadata.util.ts | 1 - ...ndard-relation-field-flat-metadata.util.ts | 1 - ...pdated-matrix-dispatcher.util.spec.ts.snap | 4 - .../flat-view-group-validator.service.ts | 46 +-- .../services/flat-view-validator.service.ts | 111 +++++++ .../constants/view-gql-fields.constants.ts | 2 +- ...cessful-find-view.integration-spec.ts.snap | 1 + .../view/create-view.integration-spec.ts | 14 + .../view/update-view.integration-spec.ts | 27 ++ .../view-group-resolver.integration-spec.ts | 72 +---- ...ny-view-groups-v2.integration-spec.ts.snap | 104 ------- ...te-many-view-groups-v2.integration-spec.ts | 152 ---------- ...te-many-view-groups-v2.integration-spec.ts | 15 +- .../graphql/utils/view-graphql.util.ts | 2 + ...tivation-deletes-views.integration-spec.ts | 10 + ...ivation-deletes-views.integration-spec.ts} | 97 +----- ...iews-kanban-properties.integration-spec.ts | 31 ++ ...-groups-side-effect-v2.integration-spec.ts | 260 ++++------------ ...eld-metadata-for-integration-tests.util.ts | 41 +++ ...-groups-side-effect-v2.integration-spec.ts | 285 ++++++++++++++++++ .../view-group.integration-spec.ts.snap | 4 +- .../suites/view-group.integration-spec.ts | 121 ++------ .../rest/suites/view.integration-spec.ts | 13 + .../test/integration/utils/view-test.util.ts | 1 - 87 files changed, 1338 insertions(+), 1067 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/compute-flat-view-groups-on-view-create.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-view/utils/handle-flat-view-update-side-effect.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception-handler.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception.filter.ts delete mode 100644 packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap delete mode 100644 packages/twenty-server/test/integration/graphql/suites/view/view-group/failing-create-many-view-groups-v2.integration-spec.ts rename packages/twenty-server/test/integration/metadata/suites/field-metadata/{view-group-field-deactivation-deletes-views.integration-spec.ts => field-group-by-deactivation-deletes-views.integration-spec.ts} (71%) create mode 100644 packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/view/update-one-view-view-groups-side-effect-v2.integration-spec.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index dcc9d7bb4a..b218954623 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -753,7 +753,6 @@ export type CoreViewGroup = { __typename?: 'CoreViewGroup'; createdAt: Scalars['DateTime']; deletedAt?: Maybe; - fieldMetadataId: Scalars['UUID']; fieldValue: Scalars['String']; id: Scalars['UUID']; isVisible: Scalars['Boolean']; @@ -963,7 +962,6 @@ export type CreateViewFilterInput = { }; export type CreateViewGroupInput = { - fieldMetadataId: Scalars['UUID']; fieldValue: Scalars['String']; id?: InputMaybe; isVisible?: InputMaybe; @@ -6069,9 +6067,9 @@ export type ViewFilterFragmentFragment = { __typename?: 'CoreViewFilter', id: st export type ViewFilterGroupFragmentFragment = { __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }; -export type ViewFragmentFragment = { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; +export type ViewFragmentFragment = { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; -export type ViewGroupFragmentFragment = { __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }; +export type ViewGroupFragmentFragment = { __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }; export type ViewSortFragmentFragment = { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }; @@ -6080,7 +6078,7 @@ export type CreateCoreViewMutationVariables = Exact<{ }>; -export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; +export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; export type CreateCoreViewFieldMutationVariables = Exact<{ input: CreateViewFieldInput; @@ -6108,7 +6106,7 @@ export type CreateCoreViewGroupMutationVariables = Exact<{ }>; -export type CreateCoreViewGroupMutation = { __typename?: 'Mutation', createCoreViewGroup: { __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type CreateCoreViewGroupMutation = { __typename?: 'Mutation', createCoreViewGroup: { __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type CreateCoreViewSortMutationVariables = Exact<{ input: CreateViewSortInput; @@ -6129,7 +6127,7 @@ export type CreateManyCoreViewGroupsMutationVariables = Exact<{ }>; -export type CreateManyCoreViewGroupsMutation = { __typename?: 'Mutation', createManyCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; +export type CreateManyCoreViewGroupsMutation = { __typename?: 'Mutation', createManyCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; export type DeleteCoreViewMutationVariables = Exact<{ id: Scalars['String']; @@ -6164,7 +6162,7 @@ export type DeleteCoreViewGroupMutationVariables = Exact<{ }>; -export type DeleteCoreViewGroupMutation = { __typename?: 'Mutation', deleteCoreViewGroup: { __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type DeleteCoreViewGroupMutation = { __typename?: 'Mutation', deleteCoreViewGroup: { __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type DeleteCoreViewSortMutationVariables = Exact<{ id: Scalars['String']; @@ -6206,7 +6204,7 @@ export type DestroyCoreViewGroupMutationVariables = Exact<{ }>; -export type DestroyCoreViewGroupMutation = { __typename?: 'Mutation', destroyCoreViewGroup: { __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type DestroyCoreViewGroupMutation = { __typename?: 'Mutation', destroyCoreViewGroup: { __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type DestroyCoreViewSortMutationVariables = Exact<{ id: Scalars['String']; @@ -6221,7 +6219,7 @@ export type UpdateCoreViewMutationVariables = Exact<{ }>; -export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; +export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; export type UpdateCoreViewFieldMutationVariables = Exact<{ input: UpdateViewFieldInput; @@ -6250,7 +6248,7 @@ export type UpdateCoreViewGroupMutationVariables = Exact<{ }>; -export type UpdateCoreViewGroupMutation = { __typename?: 'Mutation', updateCoreViewGroup: { __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type UpdateCoreViewGroupMutation = { __typename?: 'Mutation', updateCoreViewGroup: { __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type UpdateCoreViewSortMutationVariables = Exact<{ id: Scalars['String']; @@ -6263,7 +6261,7 @@ export type UpdateCoreViewSortMutation = { __typename?: 'Mutation', updateCoreVi export type FindAllCoreViewsQueryVariables = Exact<{ [key: string]: never; }>; -export type FindAllCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; +export type FindAllCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; export type FindManyCoreViewFieldsQueryVariables = Exact<{ viewId: Scalars['String']; @@ -6291,7 +6289,7 @@ export type FindManyCoreViewGroupsQueryVariables = Exact<{ }>; -export type FindManyCoreViewGroupsQuery = { __typename?: 'Query', getCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; +export type FindManyCoreViewGroupsQuery = { __typename?: 'Query', getCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; export type FindManyCoreViewSortsQueryVariables = Exact<{ viewId?: InputMaybe; @@ -6305,14 +6303,14 @@ export type FindManyCoreViewsQueryVariables = Exact<{ }>; -export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; +export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; export type FindOneCoreViewQueryVariables = Exact<{ id: Scalars['String']; }>; -export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } | null }; +export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: string, name: string, objectMetadataId: string, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: string | null, mainGroupByFieldMetadataId?: string | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: string | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: string | null, viewFields: Array<{ __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: string, fieldMetadataId: string, operand: ViewFilterOperand, value: any, viewFilterGroupId?: string | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: string, parentViewFilterGroupId?: string | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: string }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } | null }; export type FindOneCoreViewFieldQueryVariables = Exact<{ id: Scalars['String']; @@ -6340,7 +6338,7 @@ export type FindOneCoreViewGroupQueryVariables = Exact<{ }>; -export type FindOneCoreViewGroupQuery = { __typename?: 'Query', getCoreViewGroup?: { __typename?: 'CoreViewGroup', id: string, fieldMetadataId: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } | null }; +export type FindOneCoreViewGroupQuery = { __typename?: 'Query', getCoreViewGroup?: { __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null } | null }; export type FindOneCoreViewSortQueryVariables = Exact<{ id: Scalars['String']; @@ -7142,7 +7140,6 @@ export const ViewSortFragmentFragmentDoc = gql` export const ViewGroupFragmentFragmentDoc = gql` fragment ViewGroupFragment on CoreViewGroup { id - fieldMetadataId isVisible fieldValue position @@ -7165,6 +7162,7 @@ export const ViewFragmentFragmentDoc = gql` openRecordIn kanbanAggregateOperation kanbanAggregateOperationFieldMetadataId + mainGroupByFieldMetadataId anyFieldFilterValue calendarFieldMetadataId calendarLayout diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 10b89c09f8..2b01b6b0a6 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -753,7 +753,6 @@ export type CoreViewGroup = { __typename?: 'CoreViewGroup'; createdAt: Scalars['DateTime']; deletedAt?: Maybe; - fieldMetadataId: Scalars['UUID']; fieldValue: Scalars['String']; id: Scalars['UUID']; isVisible: Scalars['Boolean']; @@ -946,7 +945,6 @@ export type CreateViewFilterInput = { }; export type CreateViewGroupInput = { - fieldMetadataId: Scalars['UUID']; fieldValue: Scalars['String']; id?: InputMaybe; isVisible?: InputMaybe; @@ -4847,9 +4845,9 @@ export type ViewFilterFragmentFragment = { __typename?: 'CoreViewFilter', id: an export type ViewFilterGroupFragmentFragment = { __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }; -export type ViewFragmentFragment = { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; +export type ViewFragmentFragment = { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, mainGroupByFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; -export type ViewGroupFragmentFragment = { __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }; +export type ViewGroupFragmentFragment = { __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }; export type ViewSortFragmentFragment = { __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }; @@ -4858,7 +4856,7 @@ export type CreateCoreViewMutationVariables = Exact<{ }>; -export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; +export type CreateCoreViewMutation = { __typename?: 'Mutation', createCoreView: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, mainGroupByFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; export type CreateCoreViewFieldMutationVariables = Exact<{ input: CreateViewFieldInput; @@ -4886,7 +4884,7 @@ export type CreateCoreViewGroupMutationVariables = Exact<{ }>; -export type CreateCoreViewGroupMutation = { __typename?: 'Mutation', createCoreViewGroup: { __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type CreateCoreViewGroupMutation = { __typename?: 'Mutation', createCoreViewGroup: { __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type CreateCoreViewSortMutationVariables = Exact<{ input: CreateViewSortInput; @@ -4907,7 +4905,7 @@ export type CreateManyCoreViewGroupsMutationVariables = Exact<{ }>; -export type CreateManyCoreViewGroupsMutation = { __typename?: 'Mutation', createManyCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; +export type CreateManyCoreViewGroupsMutation = { __typename?: 'Mutation', createManyCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; export type DeleteCoreViewMutationVariables = Exact<{ id: Scalars['String']; @@ -4942,7 +4940,7 @@ export type DeleteCoreViewGroupMutationVariables = Exact<{ }>; -export type DeleteCoreViewGroupMutation = { __typename?: 'Mutation', deleteCoreViewGroup: { __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type DeleteCoreViewGroupMutation = { __typename?: 'Mutation', deleteCoreViewGroup: { __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type DeleteCoreViewSortMutationVariables = Exact<{ id: Scalars['String']; @@ -4984,7 +4982,7 @@ export type DestroyCoreViewGroupMutationVariables = Exact<{ }>; -export type DestroyCoreViewGroupMutation = { __typename?: 'Mutation', destroyCoreViewGroup: { __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type DestroyCoreViewGroupMutation = { __typename?: 'Mutation', destroyCoreViewGroup: { __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type DestroyCoreViewSortMutationVariables = Exact<{ id: Scalars['String']; @@ -4999,7 +4997,7 @@ export type UpdateCoreViewMutationVariables = Exact<{ }>; -export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; +export type UpdateCoreViewMutation = { __typename?: 'Mutation', updateCoreView: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, mainGroupByFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; export type UpdateCoreViewFieldMutationVariables = Exact<{ input: UpdateViewFieldInput; @@ -5028,7 +5026,7 @@ export type UpdateCoreViewGroupMutationVariables = Exact<{ }>; -export type UpdateCoreViewGroupMutation = { __typename?: 'Mutation', updateCoreViewGroup: { __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } }; +export type UpdateCoreViewGroupMutation = { __typename?: 'Mutation', updateCoreViewGroup: { __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } }; export type UpdateCoreViewSortMutationVariables = Exact<{ id: Scalars['String']; @@ -5041,7 +5039,7 @@ export type UpdateCoreViewSortMutation = { __typename?: 'Mutation', updateCoreVi export type FindAllCoreViewsQueryVariables = Exact<{ [key: string]: never; }>; -export type FindAllCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; +export type FindAllCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, mainGroupByFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; export type FindManyCoreViewFieldsQueryVariables = Exact<{ viewId: Scalars['String']; @@ -5069,7 +5067,7 @@ export type FindManyCoreViewGroupsQueryVariables = Exact<{ }>; -export type FindManyCoreViewGroupsQuery = { __typename?: 'Query', getCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; +export type FindManyCoreViewGroupsQuery = { __typename?: 'Query', getCoreViewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }; export type FindManyCoreViewSortsQueryVariables = Exact<{ viewId?: InputMaybe; @@ -5083,14 +5081,14 @@ export type FindManyCoreViewsQueryVariables = Exact<{ }>; -export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; +export type FindManyCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, mainGroupByFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; export type FindOneCoreViewQueryVariables = Exact<{ id: Scalars['String']; }>; -export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> } | null }; +export type FindOneCoreViewQuery = { __typename?: 'Query', getCoreView?: { __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, mainGroupByFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, visibility: ViewVisibility, createdByUserWorkspaceId?: any | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null }> } | null }; export type FindOneCoreViewFieldQueryVariables = Exact<{ id: Scalars['String']; @@ -5118,7 +5116,7 @@ export type FindOneCoreViewGroupQueryVariables = Exact<{ }>; -export type FindOneCoreViewGroupQuery = { __typename?: 'Query', getCoreViewGroup?: { __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } | null }; +export type FindOneCoreViewGroupQuery = { __typename?: 'Query', getCoreViewGroup?: { __typename?: 'CoreViewGroup', id: any, isVisible: boolean, fieldValue: string, position: number, viewId: any, createdAt: string, updatedAt: string, deletedAt?: string | null } | null }; export type FindOneCoreViewSortQueryVariables = Exact<{ id: Scalars['String']; @@ -5299,7 +5297,6 @@ export const ViewSortFragmentFragmentDoc = gql` export const ViewGroupFragmentFragmentDoc = gql` fragment ViewGroupFragment on CoreViewGroup { id - fieldMetadataId isVisible fieldValue position @@ -5322,6 +5319,7 @@ export const ViewFragmentFragmentDoc = gql` openRecordIn kanbanAggregateOperation kanbanAggregateOperationFieldMetadataId + mainGroupByFieldMetadataId anyFieldFilterValue calendarFieldMetadataId calendarLayout diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts index 4cee83a213..923ab48db2 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts @@ -42,7 +42,6 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { ({ id: v4(), __typename: 'ViewGroup', - fieldMetadataId: randomFieldForKanban, fieldValue: option.value, isVisible: true, position: index, @@ -55,14 +54,15 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { fieldValue: '', position: viewGroupsToCreate.length, isVisible: true, - fieldMetadataId: randomFieldForKanban, } satisfies ViewGroup); await createViewGroups({ - inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ - ...viewGroup, - viewId: currentViewId, - })), + createCoreViewGroupInputs: { + inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ + ...viewGroup, + viewId: currentViewId, + })), + }, }); return viewGroupsToCreate; diff --git a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithGroupRecordDrop.ts b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithGroupRecordDrop.ts index 422fc414eb..16ade26831 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithGroupRecordDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithGroupRecordDrop.ts @@ -8,12 +8,14 @@ import { originalDragSelectionComponentState } from '@/object-record/record-drag import { processGroupDrop } from '@/object-record/record-drag/utils/processGroupDrop'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; export const useProcessTableWithGroupRecordDrop = () => { @@ -47,6 +49,10 @@ export const useProcessTableWithGroupRecordDrop = () => { originalDragSelectionComponentState, ); + const groupFieldMetadata = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, + ); + const processTableWithGroupRecordDrop = useRecoilCallback( ({ snapshot }) => (result: DropResult) => { @@ -63,7 +69,7 @@ export const useProcessTableWithGroupRecordDrop = () => { } const fieldMetadata = objectMetadataItem.fields.find( - (field) => field.id === destinationRecordGroup.fieldMetadataId, + (field) => field.id === groupFieldMetadata?.id, ); if (!isDefined(fieldMetadata)) { @@ -110,14 +116,15 @@ export const useProcessTableWithGroupRecordDrop = () => { }); }, [ - currentRecordSortsCallbackState, objectMetadataItem.fields, - recordIdsByGroupFamilyState, - updateOneRow, - openModal, - selectedRowIdsSelector, originalDragSelectionCallbackState, isDraggingRecordCallbackState, + selectedRowIdsSelector, + currentRecordSortsCallbackState, + recordIdsByGroupFamilyState, + groupFieldMetadata?.id, + openModal, + updateOneRow, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupFilter.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupFilter.ts index 509d14760a..1ffb053158 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupFilter.ts @@ -1,21 +1,25 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; export const useRecordGroupFilter = (fields: FieldMetadataItem[]) => { const currentRecordGroupDefinition = useCurrentRecordGroupDefinition(); + const groupFieldMetadata = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, + ); const recordGroupFilter = useMemo(() => { if (isDefined(currentRecordGroupDefinition)) { const fieldMetadataItem = fields.find( - (fieldMetadataItem) => - fieldMetadataItem.id === currentRecordGroupDefinition.fieldMetadataId, + (fieldMetadataItem) => fieldMetadataItem.id === groupFieldMetadata?.id, ); if (!fieldMetadataItem) { throw new Error( - `Field metadata item with id ${currentRecordGroupDefinition.fieldMetadataId} not found`, + `Field metadata item with id ${groupFieldMetadata?.id} not found`, ); } @@ -31,7 +35,7 @@ export const useRecordGroupFilter = (fields: FieldMetadataItem[]) => { } return {}; - }, [currentRecordGroupDefinition, fields]); + }, [currentRecordGroupDefinition, fields, groupFieldMetadata?.id]); return { recordGroupFilter }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useReorderRecordGroups.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useReorderRecordGroups.ts index 3d0f6a09cd..2e5e86e0db 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useReorderRecordGroups.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useReorderRecordGroups.ts @@ -3,7 +3,9 @@ import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRec import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; import { type ViewType } from '@/views/types/ViewType'; @@ -37,6 +39,10 @@ export const useReorderRecordGroups = ({ const { saveViewGroups } = useSaveCurrentViewGroups(); + const groupFieldMetadata = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, + ); + const reorderRecordGroups = useRecoilCallback( ({ snapshot }) => ({ fromIndex, toIndex }: ReorderRecordGroupsParams) => { @@ -80,17 +86,23 @@ export const useReorderRecordGroups = ({ ]; }, []); - setRecordGroups( - updatedRecordGroups, + if (!isDefined(groupFieldMetadata?.id)) { + throw new Error('mainGroupByFieldMetadataId is required'); + } + + setRecordGroups({ + mainGroupByFieldMetadataId: groupFieldMetadata?.id, + recordGroups: updatedRecordGroups, recordIndexId, - objectMetadataItem.id, - ); + objectMetadataItemId: objectMetadataItem.id, + }); saveViewGroups( mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups), ); }, [ objectMetadataItem.id, + groupFieldMetadata?.id, recordIndexId, saveViewGroups, setRecordGroups, diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroups.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroups.ts index 5f1db1fab5..13983d8d90 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroups.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroups.ts @@ -16,11 +16,17 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useSetRecordGroups = () => { const setRecordGroups = useRecoilCallback( ({ snapshot, set }) => - ( - recordGroups: RecordGroupDefinition[], - recordIndexId: string, - objectMetadataItemId: string, - ) => { + ({ + mainGroupByFieldMetadataId, + recordGroups, + recordIndexId, + objectMetadataItemId, + }: { + mainGroupByFieldMetadataId: string; + recordGroups: RecordGroupDefinition[]; + recordIndexId: string; + objectMetadataItemId: string; + }) => { const objectMetadataItems = snapshot .getLoadable(objectMetadataItemsState) .getValue(); @@ -40,7 +46,8 @@ export const useSetRecordGroups = () => { instanceId: recordIndexId, }), ); - const fieldMetadataId = recordGroups?.[0]?.fieldMetadataId; + + const fieldMetadataId = mainGroupByFieldMetadataId; const fieldMetadata = fieldMetadataId ? objectMetadataItem.fields.find( (field) => field.id === fieldMetadataId, @@ -105,26 +112,34 @@ export const useSetRecordGroups = () => { ); const setRecordGroupsFromViewGroups = useCallback( - ( - viewId: string, - viewGroups: ViewGroup[], - objectMetadataItem: ObjectMetadataItem, - ) => { + ({ + viewId, + mainGroupByFieldMetadataId, + viewGroups, + objectMetadataItem, + }: { + viewId: string; + mainGroupByFieldMetadataId: string; + viewGroups: ViewGroup[]; + objectMetadataItem: ObjectMetadataItem; + }) => { const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId( objectMetadataItem.namePlural, viewId, ); const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({ + mainGroupByFieldMetadataId, objectMetadataItem, viewGroups, }); - setRecordGroups( - newGroupDefinitions, + setRecordGroups({ + mainGroupByFieldMetadataId, + recordGroups: newGroupDefinitions, recordIndexId, - objectMetadataItem.id, - ); + objectMetadataItemId: objectMetadataItem.id, + }); }, [setRecordGroups], ); diff --git a/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupDefinition.ts b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupDefinition.ts index cade958f16..1bc290bebc 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupDefinition.ts @@ -6,7 +6,6 @@ export const enum RecordGroupDefinitionType { export type RecordGroupDefinition = { id: string; - fieldMetadataId: string; type: RecordGroupDefinitionType; title: string; value: string | null; 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 cb903ed37d..47a99f92d0 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 @@ -2,17 +2,21 @@ import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups'; +import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { usePersistView } from '@/views/hooks/internal/usePersistView'; import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup'; import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; import { type ViewGroup } from '@/views/types/ViewGroup'; +import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { type CoreView } from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useHandleRecordGroupField = () => { - const { createViewGroups, deleteViewGroups } = usePersistViewGroupRecords(); + const { deleteViewGroups } = usePersistViewGroupRecords(); const currentViewIdCallbackState = useRecoilComponentCallbackState( contextStoreCurrentViewIdComponentState, @@ -25,6 +29,7 @@ export const useHandleRecordGroupField = () => { const { setRecordGroupsFromViewGroups } = useSetRecordGroups(); const { updateView } = usePersistView(); + const { loadRecordIndexStates } = useLoadRecordIndexStates(); const handleRecordGroupFieldChange = useRecoilCallback( ({ snapshot }) => @@ -50,16 +55,29 @@ export const useHandleRecordGroupField = () => { return; } - await updateView({ + const updatedViewResult = await updateView({ id: view.id, input: { mainGroupByFieldMetadataId: fieldMetadataItem.id, }, }); + if (updatedViewResult.status === 'successful') { + const updatedCoreView = updatedViewResult.response.data + ?.updateCoreView as CoreView; + + if (isDefined(updatedCoreView)) { + const updatedViewConverted = convertCoreViewToView(updatedCoreView); + await loadRecordIndexStates( + updatedViewConverted, + objectMetadataItem, + ); + } + } + const existingGroupKeys = new Set( view.viewGroups.map( - (group) => `${group.fieldMetadataId}:${group.fieldValue}`, + (group) => `${view.mainGroupByFieldMetadataId}:${group.fieldValue}`, ), ); @@ -79,7 +97,6 @@ export const useHandleRecordGroupField = () => { fieldValue: option.value, isVisible: true, position: index, - fieldMetadataId: fieldMetadataItem.id, }) satisfies ViewGroup, ); @@ -93,45 +110,23 @@ export const useHandleRecordGroupField = () => { fieldValue: '', isVisible: true, position: fieldMetadataItem.options.length, - fieldMetadataId: fieldMetadataItem.id, } satisfies ViewGroup); } - const viewGroupsToDelete = view.viewGroups.filter( - (group) => group.fieldMetadataId !== fieldMetadataItem.id, - ); - const newViewGroupsList = [ ...view.viewGroups.filter( - (group) => group.fieldMetadataId === fieldMetadataItem.id, + (_group) => + view.mainGroupByFieldMetadataId === fieldMetadataItem.id, ), ...viewGroupsToCreate, ]; - setRecordGroupsFromViewGroups( - view.id, - newViewGroupsList, + setRecordGroupsFromViewGroups({ + viewId: view.id, + mainGroupByFieldMetadataId: fieldMetadataItem.id, + viewGroups: newViewGroupsList, objectMetadataItem, - ); - - if (viewGroupsToCreate.length > 0) { - await createViewGroups({ - inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ - ...viewGroup, - viewId: view.id, - })), - }); - } - - if (viewGroupsToDelete.length > 0) { - await deleteViewGroups( - viewGroupsToDelete.map((group) => ({ - input: { - id: group.id, - }, - })), - ); - } + }); }, [ currentViewIdCallbackState, @@ -139,8 +134,7 @@ export const useHandleRecordGroupField = () => { updateView, setRecordGroupsFromViewGroups, objectMetadataItem, - createViewGroups, - deleteViewGroups, + loadRecordIndexStates, ], ); @@ -173,7 +167,16 @@ export const useHandleRecordGroupField = () => { })), ); - setRecordGroupsFromViewGroups(view.id, [], objectMetadataItem); + if (!isDefined(view.mainGroupByFieldMetadataId)) { + throw new Error('mainGroupByFieldMetadataId is required'); + } + + setRecordGroupsFromViewGroups({ + viewId: view.id, + mainGroupByFieldMetadataId: view.mainGroupByFieldMetadataId, + viewGroups: [], + objectMetadataItem, + }); }, [ deleteViewGroups, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts index c3eda9e8fa..f0ab76d017 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts @@ -178,11 +178,12 @@ export const useLoadRecordIndexStates = () => { onViewFieldsChange(view.viewFields, objectMetadataItem); - setRecordGroupsFromViewGroups( - view.id, - view.viewGroups, + setRecordGroupsFromViewGroups({ + viewId: view.id, + mainGroupByFieldMetadataId: view.mainGroupByFieldMetadataId ?? '', + viewGroups: view.viewGroups, objectMetadataItem, - ); + }); setContextStoreTargetedRecordsRuleComponentState((prev) => ({ ...prev, @@ -199,9 +200,9 @@ export const useLoadRecordIndexStates = () => { view.calendarFieldMetadataId ?? null, ); - if (isDefined(view.viewGroups?.[0]?.fieldMetadataId)) { + if (isDefined(view.mainGroupByFieldMetadataId)) { const recordIndexGroupFieldMetadataItemId = - view.viewGroups?.[0]?.fieldMetadataId; + view.mainGroupByFieldMetadataId; const { fieldMetadataItem: recordIndexGroupFieldMetadataItem } = getFieldMetadataItemByIdOrThrow( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx index a5170be84d..48d6147877 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx @@ -1,9 +1,11 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { t } from '@lingui/core/macro'; import { useRecoilValue } from 'recoil'; import { IconPlus } from 'twenty-ui/display'; @@ -17,12 +19,16 @@ export const RecordTableRecordGroupSectionAddNew = () => { recordGroupDefinitionFamilyState(currentRecordGroupId), ); + const mainGroupByFieldMetadata = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, + ); + const { createNewIndexRecord } = useCreateNewIndexRecord({ objectMetadataItem, }); const fieldMetadataItem = objectMetadataItem.fields.find( - (field) => field.id === recordGroup?.fieldMetadataId, + (field) => field.id === mainGroupByFieldMetadata?.id, ); const objectPermissions = useObjectPermissionsForObject( diff --git a/packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts b/packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts index f4a48510d8..c90c22a5e9 100644 --- a/packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts +++ b/packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts @@ -24,7 +24,7 @@ export const VIEW_FRAGMENT = gql` openRecordIn kanbanAggregateOperation kanbanAggregateOperationFieldMetadataId - # mainGroupByFieldMetadataId + mainGroupByFieldMetadataId anyFieldFilterValue calendarFieldMetadataId calendarLayout diff --git a/packages/twenty-front/src/modules/views/graphql/fragments/viewGroupFragment.ts b/packages/twenty-front/src/modules/views/graphql/fragments/viewGroupFragment.ts index 777b1b1cd5..c0dfcb3c0d 100644 --- a/packages/twenty-front/src/modules/views/graphql/fragments/viewGroupFragment.ts +++ b/packages/twenty-front/src/modules/views/graphql/fragments/viewGroupFragment.ts @@ -3,7 +3,6 @@ import { gql } from '@apollo/client'; export const VIEW_GROUP_FRAGMENT = gql` fragment ViewGroupFragment on CoreViewGroup { id - fieldMetadataId isVisible fieldValue position diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts index 034083d9a6..bd728989e2 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts @@ -32,9 +32,11 @@ export const usePersistViewGroupRecords = () => { const { enqueueErrorSnackBar } = useSnackBar(); const createViewGroups = useCallback( - async ( - createCoreViewGroupInputs: CreateManyCoreViewGroupsMutationVariables, - ): Promise< + async ({ + createCoreViewGroupInputs, + }: { + createCoreViewGroupInputs: CreateManyCoreViewGroupsMutationVariables; + }): Promise< MetadataRequestResult > | null> diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index a49740db2c..64d7e2c568 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -11,9 +11,9 @@ import { usePersistView } from '@/views/hooks/internal/usePersistView'; import { usePersistViewField } from '@/views/hooks/internal/usePersistViewField'; import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilter'; import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroup'; -import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup'; import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSort'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; +import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect'; import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState'; import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector'; import { type GraphQLView } from '@/views/types/GraphQLView'; @@ -28,7 +28,10 @@ import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; -import { ViewCalendarLayout } from '~/generated-metadata/graphql'; +import { + type CoreViewGroup, + ViewCalendarLayout, +} from '~/generated-metadata/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { @@ -47,7 +50,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { const { createViewSorts } = usePersistViewSortRecords(); - const { createViewGroups } = usePersistViewGroupRecords(); + const { triggerViewGroupOptimisticEffect } = + useTriggerViewGroupOptimisticEffect(); const { createViewFilters } = usePersistViewFilterRecords(); @@ -182,7 +186,10 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { } if (type === ViewType.Kanban) { - if (!isDefined(mainGroupByFieldMetadataId)) { + if ( + !isDefined(mainGroupByFieldMetadataId) || + mainGroupByFieldMetadataId === '' + ) { throw new Error('Kanban view must have a kanban field'); } @@ -194,33 +201,38 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { ({ id: v4(), __typename: 'ViewGroup', - fieldMetadataId: mainGroupByFieldMetadataId, fieldValue: option.value, isVisible: true, position: index, }) satisfies ViewGroup, ) ?? []; - viewGroupsToCreate.push({ - __typename: 'ViewGroup', - id: v4(), - fieldValue: '', - position: viewGroupsToCreate.length, - isVisible: true, - fieldMetadataId: mainGroupByFieldMetadataId, - } satisfies ViewGroup); - - const groupResult = await createViewGroups({ - inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ - ...viewGroup, - viewId: newViewId, - })), - }); - - if (groupResult.status === 'failed') { - set(isPersistingViewFieldsState, false); - return undefined; + if ( + objectMetadataItem.fields.find( + (field) => field.id === mainGroupByFieldMetadataId, + )?.isNullable === true + ) { + viewGroupsToCreate.push({ + __typename: 'ViewGroup', + id: v4(), + fieldValue: '', + position: viewGroupsToCreate.length, + isVisible: true, + } satisfies ViewGroup); } + + triggerViewGroupOptimisticEffect({ + createdViewGroups: viewGroupsToCreate.map( + ({ __typename, ...viewGroup }) => + ({ + ...viewGroup, + viewId: newViewId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + deletedAt: null, + }) as Omit, + ), + }); } if (shouldCopyFiltersAndSortsAndAggregate === true) { @@ -292,7 +304,6 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { anyFieldFilterValue, objectMetadataItem.fields, objectMetadataItem.id, - createViewGroups, currentRecordFilterGroups, currentRecordFilters, currentRecordSorts, @@ -300,6 +311,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { createViewFilters, createViewSorts, refreshCoreViewsByObjectMetadataId, + triggerViewGroupOptimisticEffect, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useGetViewGroupsFilters.ts b/packages/twenty-front/src/modules/views/hooks/useGetViewGroupsFilters.ts index 050662e96b..89bfbb1d69 100644 --- a/packages/twenty-front/src/modules/views/hooks/useGetViewGroupsFilters.ts +++ b/packages/twenty-front/src/modules/views/hooks/useGetViewGroupsFilters.ts @@ -10,9 +10,13 @@ export const useGetViewGroupsFilters = (): RecordFilter[] => { currentView?.viewGroups .filter((recordGroup) => !recordGroup.isVisible) .map((recordGroup) => { + if (!isDefined(currentView.mainGroupByFieldMetadataId)) { + throw new Error('mainGroupByFieldMetadataId is required'); + } + return { id: recordGroup.id, - fieldMetadataId: recordGroup.fieldMetadataId, + fieldMetadataId: currentView.mainGroupByFieldMetadataId, value: JSON.stringify([recordGroup.fieldValue]), operand: ViewFilterOperand.IS_NOT, displayValue: '', diff --git a/packages/twenty-front/src/modules/views/hooks/useRefreshAllCoreViews.ts b/packages/twenty-front/src/modules/views/hooks/useRefreshAllCoreViews.ts index 1a690360b8..de7464c7e4 100644 --- a/packages/twenty-front/src/modules/views/hooks/useRefreshAllCoreViews.ts +++ b/packages/twenty-front/src/modules/views/hooks/useRefreshAllCoreViews.ts @@ -24,11 +24,13 @@ export const useRefreshAllCoreViews = ( .getLoadable(coreViewsState) .getValue(); + const coreViewsFromResult = result.data.getCoreViews; + if ( isDefined(result.data?.getCoreViews) && - !isDeeplyEqual(currentCoreViews, result.data.getCoreViews) + !isDeeplyEqual(currentCoreViews, coreViewsFromResult) ) { - set(coreViewsState, result.data.getCoreViews); + set(coreViewsState, coreViewsFromResult); } return result.data?.getCoreViews; diff --git a/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts b/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts index b041814f3a..220c976e9d 100644 --- a/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts +++ b/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts @@ -49,18 +49,18 @@ export const useRefreshCoreViewsByObjectMetadataId = () => { ) .getValue(); - if ( - isDeeplyEqual(coreViewsForObjectMetadataId, result.data.getCoreViews) - ) { + const coreViewsFromResult = result.data.getCoreViews; + + if (isDeeplyEqual(coreViewsForObjectMetadataId, coreViewsFromResult)) { return; } set( coreViewsByObjectMetadataIdFamilySelector(objectMetadataId), - result.data.getCoreViews, + coreViewsFromResult, ); - for (const coreView of result.data.getCoreViews) { + for (const coreView of coreViewsFromResult) { const existingView = coreViewsForObjectMetadataId.find( (coreViewForObjectMetadata) => coreViewForObjectMetadata.id === coreView.id, diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts index 740b70a21e..fbba95e40a 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts @@ -45,9 +45,7 @@ export const useSaveCurrentViewGroups = () => { const existingField = currentViewGroups.find( (currentViewGroup) => - currentViewGroup.fieldValue === viewGroupToSave.fieldValue && - currentViewGroup.fieldMetadataId === - viewGroupToSave.fieldMetadataId, + currentViewGroup.fieldValue === viewGroupToSave.fieldValue, ); if (isUndefinedOrNull(existingField)) { @@ -76,7 +74,6 @@ export const useSaveCurrentViewGroups = () => { update: { isVisible: viewGroupToSave.isVisible, position: viewGroupToSave.position, - fieldMetadataId: viewGroupToSave.fieldMetadataId, fieldValue: viewGroupToSave.fieldValue, }, }, @@ -118,9 +115,7 @@ export const useSaveCurrentViewGroups = () => { .map((viewGroupToSave) => { const existingField = currentViewGroups.find( (currentViewGroup) => - currentViewGroup.fieldValue === viewGroupToSave.fieldValue && - currentViewGroup.fieldMetadataId === - viewGroupToSave.fieldMetadataId, + currentViewGroup.fieldValue === viewGroupToSave.fieldValue, ); if (isUndefinedOrNull(existingField)) { @@ -148,7 +143,6 @@ export const useSaveCurrentViewGroups = () => { update: { isVisible: viewGroupToSave.isVisible, position: viewGroupToSave.position, - fieldMetadataId: viewGroupToSave.fieldMetadataId, fieldValue: viewGroupToSave.fieldValue, }, }, @@ -160,18 +154,22 @@ export const useSaveCurrentViewGroups = () => { (viewFieldToSave) => !currentViewGroups.some( (currentViewGroup) => - currentViewGroup.fieldValue === viewFieldToSave.fieldValue && - currentViewGroup.fieldMetadataId === - viewFieldToSave.fieldMetadataId, + currentViewGroup.fieldValue === viewFieldToSave.fieldValue, ), ); + if (!isDefined(view.mainGroupByFieldMetadataId)) { + throw new Error('mainGroupByFieldMetadataId is required'); + } + await Promise.all([ createViewGroups({ - inputs: viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ - ...viewGroup, - viewId: view.id, - })), + createCoreViewGroupInputs: { + inputs: viewGroupsToCreate.map((viewGroupToCreate) => ({ + ...viewGroupToCreate, + viewId: view.id, + })), + }, }), updateViewGroups(viewGroupsToUpdate), ]); diff --git a/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect.ts b/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect.ts index a449aa6d7f..5d3f848c98 100644 --- a/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect.ts @@ -6,6 +6,7 @@ import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { type CoreViewGroup } from '~/generated/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; + type UpdatedDeletedCoreViewGroup = { createdViewGroups?: Omit[]; updatedViewGroups?: Omit[]; diff --git a/packages/twenty-front/src/modules/views/types/ViewGroup.ts b/packages/twenty-front/src/modules/views/types/ViewGroup.ts index 9f0cd38222..a8e9a41904 100644 --- a/packages/twenty-front/src/modules/views/types/ViewGroup.ts +++ b/packages/twenty-front/src/modules/views/types/ViewGroup.ts @@ -1,7 +1,6 @@ export type ViewGroup = { __typename: 'ViewGroup'; id: string; - fieldMetadataId: string; isVisible: boolean; fieldValue: string; position: number; diff --git a/packages/twenty-front/src/modules/views/utils/convertCoreViewGroupToViewGroup.ts b/packages/twenty-front/src/modules/views/utils/convertCoreViewGroupToViewGroup.ts index fe14f48125..210a8536b1 100644 --- a/packages/twenty-front/src/modules/views/utils/convertCoreViewGroupToViewGroup.ts +++ b/packages/twenty-front/src/modules/views/utils/convertCoreViewGroupToViewGroup.ts @@ -4,13 +4,12 @@ import { type CoreViewGroup } from '~/generated/graphql'; export const convertCoreViewGroupToViewGroup = ( coreViewGroup: Pick< CoreViewGroup, - 'id' | 'fieldMetadataId' | 'isVisible' | 'fieldValue' | 'position' + 'id' | 'isVisible' | 'fieldValue' | 'position' >, ): ViewGroup => { return { __typename: 'ViewGroup', id: coreViewGroup.id, - fieldMetadataId: coreViewGroup.fieldMetadataId, isVisible: coreViewGroup.isVisible, fieldValue: coreViewGroup.fieldValue, position: coreViewGroup.position, diff --git a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts index 61b055a9c6..8e6bca62ff 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts @@ -8,9 +8,11 @@ import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const mapViewGroupsToRecordGroupDefinitions = ({ + mainGroupByFieldMetadataId, objectMetadataItem, viewGroups, }: { + mainGroupByFieldMetadataId: string; objectMetadataItem: ObjectMetadataItem; viewGroups: ViewGroup[]; }): RecordGroupDefinition[] => { @@ -18,10 +20,10 @@ export const mapViewGroupsToRecordGroupDefinitions = ({ return []; } - const fieldMetadataId = viewGroups?.[0]?.fieldMetadataId; const selectFieldMetadataItem = objectMetadataItem.fields.find( (field) => - field.id === fieldMetadataId && field.type === FieldMetadataType.SELECT, + field.id === mainGroupByFieldMetadataId && + field.type === FieldMetadataType.SELECT, ); if (!selectFieldMetadataItem) { @@ -36,10 +38,6 @@ export const mapViewGroupsToRecordGroupDefinitions = ({ const recordGroupDefinitionsFromViewGroups = viewGroups .map((viewGroup) => { - if (viewGroup.fieldMetadataId !== selectFieldMetadataItem.id) { - return null; - } - const selectedOption = selectFieldMetadataItem.options?.find( (option) => option.value === viewGroup.fieldValue, ); @@ -50,7 +48,6 @@ export const mapViewGroupsToRecordGroupDefinitions = ({ return { id: viewGroup.id, - fieldMetadataId: viewGroup.fieldMetadataId, type: !isDefined(selectedOption) ? RecordGroupDefinitionType.NoValue : RecordGroupDefinitionType.Value, diff --git a/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts b/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts index bc5f34f079..de711674ab 100644 --- a/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts +++ b/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts @@ -7,7 +7,6 @@ export const recordGroupDefinitionToViewGroup = ( return { __typename: 'ViewGroup', id: recordGroup.id, - fieldMetadataId: recordGroup.fieldMetadataId, position: recordGroup.position, isVisible: recordGroup.isVisible ?? true, fieldValue: recordGroup.value ?? '', diff --git a/packages/twenty-front/src/testing/mock-data/views.ts b/packages/twenty-front/src/testing/mock-data/views.ts index 7c842eb36a..ce76e60847 100644 --- a/packages/twenty-front/src/testing/mock-data/views.ts +++ b/packages/twenty-front/src/testing/mock-data/views.ts @@ -28,6 +28,7 @@ export const mockedViewsData: View[] = [ type: ViewType.Table, icon: 'IconSkyline', key: ViewKey.Index, + mainGroupByFieldMetadataId: null, kanbanAggregateOperation: AggregateOperations.COUNT, kanbanAggregateOperationFieldMetadataId: '', position: 0, @@ -48,6 +49,7 @@ export const mockedViewsData: View[] = [ type: ViewType.Table, icon: 'IconPerson', key: ViewKey.Index, + mainGroupByFieldMetadataId: null, kanbanAggregateOperation: AggregateOperations.COUNT, kanbanAggregateOperationFieldMetadataId: '', position: 0, @@ -68,6 +70,7 @@ export const mockedViewsData: View[] = [ type: ViewType.Kanban, icon: 'IconOpportunity', key: ViewKey.Index, + mainGroupByFieldMetadataId: null, kanbanAggregateOperation: AggregateOperations.COUNT, kanbanAggregateOperationFieldMetadataId: '', position: 0, @@ -88,6 +91,7 @@ export const mockedViewsData: View[] = [ type: ViewType.Table, icon: 'IconSkyline', key: null, + mainGroupByFieldMetadataId: null, kanbanAggregateOperation: AggregateOperations.COUNT, kanbanAggregateOperationFieldMetadataId: '', position: 0, diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index 1532c629aa..e790bfb3ef 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -1008,7 +1008,7 @@ export class ConfigVariables { description: 'Maximum complexity allowed for Common API queries', type: ConfigVariableType.NUMBER, }) - COMMON_QUERY_COMPLEXITY_LIMIT = 50; + COMMON_QUERY_COMPLEXITY_LIMIT = 2000; @ConfigVariablesMetadata({ group: ConfigVariablesGroup.RATE_LIMITING, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts index 6996393332..40afeaba26 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant.ts @@ -65,10 +65,6 @@ export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = { metadataName: 'view', flatEntityForeignKeyAggregator: 'viewGroupIds', }, - fieldMetadataId: { - metadataName: 'fieldMetadata', - flatEntityForeignKeyAggregator: 'viewGroupIds', - }, }, index: { objectMetadataId: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.spec.ts index b45634af79..be795fc134 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.spec.ts @@ -27,7 +27,7 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow', applicationId, }); - const mockFieldMEtadata = getFlatFieldMetadataMock({ + const mockFieldMetadata = getFlatFieldMetadataMock({ objectMetadataId, id: '202020-71a3-4856-a3d0-d08cea0ecec6', type: FieldMetadataType.DATE, @@ -35,9 +35,9 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow', applicationId, universalIdentifier: 'field-universal-1', viewFieldIds: [], - viewGroupIds: [], viewFilterIds: [], calendarViewIds: [], + mainGroupByFieldMetadataViewIds: [], }); const mockView: Pick & Partial = { @@ -49,13 +49,13 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow', viewFilterIds: [], viewGroupIds: [], applicationId, - calendarFieldMetadataId: mockFieldMEtadata.id, + calendarFieldMetadataId: mockFieldMetadata.id, }; const flatEntityAndRelatedMapsToMutate: MetadataFlatEntityAndRelatedFlatEntityMaps<'view'> = { flatFieldMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: mockFieldMEtadata, + flatEntity: mockFieldMetadata, flatEntityMaps: createEmptyFlatEntityMaps(), }), flatObjectMetadataMaps: addFlatEntityToFlatEntityMapsOrThrow({ @@ -85,7 +85,7 @@ describe('addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow', expect( flatEntityAndRelatedMapsToMutate.flatFieldMetadataMaps.byId[ - mockFieldMEtadata.id + mockFieldMetadata.id ], ).toMatchObject>({ calendarViewIds: [mockView.id], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.spec.ts index 576f20a0ad..45c63bc734 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.spec.ts @@ -34,10 +34,10 @@ describe('deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThr type: FieldMetadataType.DATE, universalIdentifier: 'field-universal-1', viewFieldIds: [], - viewGroupIds: [], viewFilterIds: [], workspaceId, calendarViewIds: [viewId], + mainGroupByFieldMetadataViewIds: [], applicationId, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts index 5cc854ba59..de203c6421 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts @@ -21,7 +21,6 @@ export const getFlatFieldMetadataMock = ( return { calendarViewIds: [], viewFilterIds: [], - viewGroupIds: [], kanbanAggregateOperationViewIds: [], viewFieldIds: [], createdAt, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts index 7e59d81425..b2b28f74b0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts @@ -35,7 +35,6 @@ export const getRelationTargetFlatFieldMetadataMock = ({ calendarViewIds: [], mainGroupByFieldMetadataViewIds: [], viewFilterIds: [], - viewGroupIds: [], viewFieldIds: [], kanbanAggregateOperationViewIds: [], createdAt, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap index ee87a14a4d..b38145fccd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap @@ -137,7 +137,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "viewFieldIds": [], "viewFilterIds": [], - "viewGroupIds": [], "workspaceId": Any, }, { @@ -176,7 +175,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "viewFieldIds": [], "viewFilterIds": [], - "viewGroupIds": [], "workspaceId": Any, }, { @@ -213,7 +211,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "viewFieldIds": [], "viewFilterIds": [], - "viewGroupIds": [], "workspaceId": Any, }, { @@ -252,7 +249,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "viewFieldIds": [], "viewFilterIds": [], - "viewGroupIds": [], "workspaceId": Any, }, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts index 637ae6b4f0..cdf44c2d26 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts @@ -41,43 +41,36 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro async computeForCache( workspaceId: string, ): Promise> { - const [fieldMetadatas, viewFields, viewFilters, viewGroups, views] = - await Promise.all([ - this.fieldMetadataRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - this.viewFieldRepository.find({ - where: { workspaceId }, - select: ['id', 'fieldMetadataId'], - withDeleted: true, - }), - this.viewFilterRepository.find({ - where: { workspaceId }, - select: ['id', 'fieldMetadataId'], - withDeleted: true, - }), - this.viewGroupRepository.find({ - where: { workspaceId }, - select: ['id', 'fieldMetadataId'], - withDeleted: true, - }), - this.viewRepository.find({ - where: { workspaceId }, - select: [ - 'id', - 'kanbanAggregateOperationFieldMetadataId', - 'calendarFieldMetadataId', - 'mainGroupByFieldMetadataId', - ], - withDeleted: true, - }), - ]); + const [fieldMetadatas, viewFields, viewFilters, views] = await Promise.all([ + this.fieldMetadataRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.viewFieldRepository.find({ + where: { workspaceId }, + select: ['id', 'fieldMetadataId'], + withDeleted: true, + }), + this.viewFilterRepository.find({ + where: { workspaceId }, + select: ['id', 'fieldMetadataId'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: [ + 'id', + 'kanbanAggregateOperationFieldMetadataId', + 'calendarFieldMetadataId', + 'mainGroupByFieldMetadataId', + ], + withDeleted: true, + }), + ]); const [ viewFieldsByFieldId, viewFiltersByFieldId, - viewGroupsByFieldId, calendarViewsByFieldId, kanbanViewsByFieldId, mainGroupByFieldMetadataViewsByFieldId, @@ -91,10 +84,6 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro entities: viewFilters, foreignKey: 'fieldMetadataId', }, - { - entities: viewGroups, - foreignKey: 'fieldMetadataId', - }, { entities: views, foreignKey: 'calendarFieldMetadataId', @@ -117,7 +106,6 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro ...fieldMetadataEntity, viewFields: viewFieldsByFieldId.get(fieldMetadataEntity.id) || [], viewFilters: viewFiltersByFieldId.get(fieldMetadataEntity.id) || [], - viewGroups: viewGroupsByFieldId.get(fieldMetadataEntity.id) || [], kanbanAggregateOperationViews: kanbanViewsByFieldId.get(fieldMetadataEntity.id) || [], calendarViews: calendarViewsByFieldId.get(fieldMetadataEntity.id) || [], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts index 031176ef32..5eb28f028a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts @@ -14,19 +14,20 @@ export const FIELD_METADATA_RELATION_PROPERTIES = [ 'kanbanAggregateOperationViews', 'calendarViews', 'mainGroupByFieldMetadataViews', - 'viewGroups', ] as const satisfies (keyof FieldMetadataEntity)[]; export type FieldMetadataEntityRelationProperties = (typeof FIELD_METADATA_RELATION_PROPERTIES)[number]; export type FlatFieldMetadata = - Omit, FieldMetadataEntityRelationProperties> & { + Omit< + FieldMetadataEntity, + FieldMetadataEntityRelationProperties | 'viewGroups' + > & { universalIdentifier: string; viewFieldIds: string[]; viewFilterIds: string[]; kanbanAggregateOperationViewIds: string[]; calendarViewIds: string[]; mainGroupByFieldMetadataViewIds: string[]; - viewGroupIds: string[]; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts index e0263c8578..c1809a0216 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts @@ -26,7 +26,6 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = < mainGroupByFieldMetadataViewIds: fieldMetadataEntity.mainGroupByFieldMetadataViews?.map(({ id }) => id) ?? [], - viewGroupIds: fieldMetadataEntity.viewGroups.map(({ id }) => id), viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id), viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id), universalIdentifier: diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts index 1f1be6174d..9efca01632 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts @@ -60,7 +60,6 @@ export const getDefaultFlatFieldMetadata = ({ morphId: null, applicationId: workspaceCustomApplicationId, viewFilterIds: [], - viewGroupIds: [], kanbanAggregateOperationViewIds: [], } as const satisfies FlatFieldMetadata; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util.ts index 0c5a43935f..feebe902b2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util.ts @@ -17,7 +17,10 @@ type HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs = FromTo< FlatFieldMetadata, 'flatFieldMetadata' > & - Pick; + Pick< + AllFlatEntityMaps, + 'flatViewFilterMaps' | 'flatViewGroupMaps' | 'flatViewMaps' + >; type EnumFieldMetadataSideEffectResult = FlatViewGroupsToDeleteUpdateAndCreate & FlatViewFiltersToDeleteAndUpdate; @@ -36,6 +39,7 @@ export const handleEnumFlatFieldMetadataUpdateSideEffects = ({ toFlatFieldMetadata, flatViewFilterMaps, flatViewGroupMaps, + flatViewMaps, }: HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs): EnumFieldMetadataSideEffectResult => { const sideEffectResult = structuredClone( EMPTY_ENUM_FIELD_METADATA_SIDE_EFFECT_RESULT, @@ -69,6 +73,7 @@ export const handleEnumFlatFieldMetadataUpdateSideEffects = ({ flatViewGroupsToDelete, flatViewGroupsToUpdate, } = recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate({ + flatViewMaps, flatViewGroupMaps, fromFlatFieldMetadata, update: optionsPropertyUpdate, @@ -82,6 +87,7 @@ export const handleEnumFlatFieldMetadataUpdateSideEffects = ({ if (fromFlatFieldMetadata.isNullable !== toFlatFieldMetadata.isNullable) { const { flatViewGroupsToCreate, flatViewGroupsToDelete } = recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate({ + flatViewMaps, flatViewGroupMaps, fromFlatFieldMetadata, toFlatFieldMetadata, 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 0f3a320637..897fb699a5 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 @@ -46,8 +46,16 @@ export const handleFieldMetadataDeactivationSideEffects = ({ flatEntityMaps: flatViewFieldMaps, }); + const flatViewsAffected: FlatView[] = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds, + flatEntityMaps: flatViewMaps, + }); + const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityIds: fromFlatFieldMetadata.viewGroupIds, + flatEntityIds: flatViewsAffected.flatMap( + (flatView) => flatView.viewGroupIds, + ), flatEntityMaps: flatViewGroupMaps, }); @@ -60,6 +68,7 @@ export const handleFieldMetadataDeactivationSideEffects = ({ ...new Set([ ...Object.keys(flatViewGroupRecordByViewId), ...fromFlatFieldMetadata.calendarViewIds, + ...fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds, ]), ]; 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 8366bae4d2..c31354e646 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 @@ -100,6 +100,7 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({ flatViewGroupsToDelete, flatViewGroupsToUpdate, } = handleEnumFlatFieldMetadataUpdateSideEffects({ + flatViewMaps, flatViewFilterMaps, flatViewGroupMaps, fromFlatFieldMetadata, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util.ts index 1d4a8f1d46..a5ebb05182 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util.ts @@ -12,7 +12,7 @@ type RecomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdateArgs = FromTo< FlatFieldMetadata, 'flatFieldMetadata' > & - Pick; + Pick; type EnumFieldMetadataIsNullableUpdateSideEffect = { flatViewGroupsToDelete: FlatViewGroup[]; @@ -25,6 +25,7 @@ const EMPTY_ENUM_FIELD_METADATA_IS_NULLABLE_UPDATE_SIDE_EFFECT_RESULT: EnumField }; export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({ + flatViewMaps, flatViewGroupMaps: allFlatViewGroups, fromFlatFieldMetadata, toFlatFieldMetadata, @@ -35,8 +36,15 @@ export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({ const sideEffectResult = structuredClone( EMPTY_ENUM_FIELD_METADATA_IS_NULLABLE_UPDATE_SIDE_EFFECT_RESULT, ); + const flatViewsAffected = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds, + flatEntityMaps: flatViewMaps, + }); + const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityIds: fromFlatFieldMetadata.viewGroupIds, + flatEntityIds: flatViewsAffected.flatMap( + (flatView) => flatView.viewGroupIds, + ), flatEntityMaps: allFlatViewGroups, }); const { flatViewGroupRecordByViewId, highestViewGroupPositionByViewId } = @@ -60,7 +68,6 @@ export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({ const createdAt = new Date(); sideEffectResult.flatViewGroupsToCreate.push({ - fieldMetadataId: toFlatFieldMetadata.id, id: viewGroupId, universalIdentifier: viewGroupId, fieldValue: '', @@ -72,6 +79,7 @@ export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({ deletedAt: null, viewId, applicationId: toFlatFieldMetadata.applicationId, + fieldMetadataId: fromFlatFieldMetadata.id, }); } else if (isDefined(emptyValueFlatViewGroup)) { sideEffectResult.flatViewGroupsToDelete.push(emptyValueFlatViewGroup); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util.ts index 0b6bcfb7fb..7db865d066 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util.ts @@ -17,7 +17,7 @@ import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migr type RecomputeViewGroupsOnFlatFieldMetadataOptionsUpdateArgs = { fromFlatFieldMetadata: FlatFieldMetadata; update: PropertyUpdate, 'options'>; -} & Pick; +} & Pick; export type FlatViewGroupsToDeleteUpdateAndCreate = { flatViewGroupsToDelete: FlatViewGroup[]; @@ -25,6 +25,7 @@ export type FlatViewGroupsToDeleteUpdateAndCreate = { flatViewGroupsToCreate: FlatViewGroup[]; }; export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({ + flatViewMaps, flatViewGroupMaps, fromFlatFieldMetadata, update, @@ -39,8 +40,15 @@ export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({ toOptions: update.to, }); + const flatViewsAffected = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: fromFlatFieldMetadata.mainGroupByFieldMetadataViewIds, + flatEntityMaps: flatViewMaps, + }); + const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityIds: fromFlatFieldMetadata.viewGroupIds, + flatEntityIds: flatViewsAffected.flatMap( + (flatView) => flatView.viewGroupIds, + ), flatEntityMaps: flatViewGroupMaps, }); @@ -63,8 +71,7 @@ export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({ const flatViewGroupsToUpdate = updatedFieldMetadataOptions.flatMap( ({ from: fromOption, to: toOption }) => flatViewGroups.flatMap((flatViewGroup) => - flatViewGroup.fieldValue === fromOption.value && - flatViewGroup.fieldMetadataId === fromFlatFieldMetadata.id + flatViewGroup.fieldValue === fromOption.value ? { ...flatViewGroup, fieldValue: toOption.value } : [], ), diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/constants/view-group-entity-relation-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/constants/view-group-entity-relation-properties.constant.ts index 1876833055..e96a521508 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/constants/view-group-entity-relation-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/constants/view-group-entity-relation-properties.constant.ts @@ -1,7 +1,6 @@ import { type ViewGroupEntityRelationProperties } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; export const VIEW_GROUP_ENTITY_RELATION_PROPERTIES = [ - 'fieldMetadata', 'view', 'workspace', ] as const satisfies ViewGroupEntityRelationProperties[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/compute-flat-view-groups-on-view-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/compute-flat-view-groups-on-view-create.util.ts new file mode 100644 index 0000000000..95871b30ed --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/compute-flat-view-groups-on-view-create.util.ts @@ -0,0 +1,76 @@ +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; + +type ComputeFlatViewGroupsOnViewCreateArgs = { + flatViewToCreateId: string; + mainGroupByFieldMetadataId: string; +} & Pick; + +export const computeFlatViewGroupsOnViewCreate = ({ + flatViewToCreateId, + mainGroupByFieldMetadataId, + flatFieldMetadataMaps, +}: ComputeFlatViewGroupsOnViewCreateArgs): FlatViewGroup[] => { + const mainGroupByFieldMetadata = + flatFieldMetadataMaps.byId[mainGroupByFieldMetadataId]; + + if (!isDefined(mainGroupByFieldMetadata)) { + throw new FlatEntityMapsException( + 'mainGroupByFieldMetadataId not found', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const createdAt = new Date(); + + const flatViewGroupsFromOptions: FlatViewGroup[] = ( + mainGroupByFieldMetadata.options ?? [] + ).map((option, index) => { + const viewGroupId = v4(); + + return { + id: viewGroupId, + fieldMetadataId: mainGroupByFieldMetadata.id, + viewId: flatViewToCreateId, + workspaceId: mainGroupByFieldMetadata.workspaceId, + createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: viewGroupId, + isVisible: true, + fieldValue: option.value, + position: index, + applicationId: mainGroupByFieldMetadata.applicationId, + }; + }); + + const flatViewGroups: FlatViewGroup[] = [...flatViewGroupsFromOptions]; + + if (mainGroupByFieldMetadata.isNullable === true) { + const emptyGroupId = v4(); + + flatViewGroups.push({ + id: emptyGroupId, + fieldMetadataId: mainGroupByFieldMetadata.id, + viewId: flatViewToCreateId, + workspaceId: mainGroupByFieldMetadata.workspaceId, + createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: emptyGroupId, + isVisible: true, + fieldValue: '', + position: flatViewGroupsFromOptions.length, + applicationId: mainGroupByFieldMetadata.applicationId, + }); + } + + return flatViewGroups; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util.ts index 872bcac15a..a7bad09dd3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util.ts @@ -8,15 +8,17 @@ export const fromCreateViewGroupInputToFlatViewGroupToCreate = ({ createViewGroupInput: rawCreateViewGroupInput, workspaceId, workspaceCustomApplicationId, + mainGroupByFieldMetadataId, }: { createViewGroupInput: CreateViewGroupInput; workspaceId: string; workspaceCustomApplicationId: string; + mainGroupByFieldMetadataId: string; }): FlatViewGroup => { - const { fieldMetadataId, viewId, ...createViewGroupInput } = + const { viewId, ...createViewGroupInput } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawCreateViewGroupInput, - ['fieldMetadataId', 'fieldValue', 'id', 'viewId'], + ['fieldValue', 'id', 'viewId'], ); const createdAt = new Date(); @@ -24,7 +26,7 @@ export const fromCreateViewGroupInputToFlatViewGroupToCreate = ({ return { id: viewGroupId, - fieldMetadataId, + fieldMetadataId: mainGroupByFieldMetadataId, // Mandatory because non-nullable until we completely remove it viewId, workspaceId, createdAt: createdAt, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util.ts index 1359b123c8..b44cf130bd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util.ts @@ -1,6 +1,12 @@ -import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; +import { + isDefined, + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, +} from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; +import { computeFlatViewGroupsOnViewCreate } from 'src/engine/metadata-modules/flat-view-group/utils/compute-flat-view-groups-on-view-create.util'; import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; import { type CreateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/create-view.input'; import { ViewOpenRecordIn } from 'src/engine/metadata-modules/view/enums/view-open-record-in'; @@ -12,12 +18,17 @@ export const fromCreateViewInputToFlatViewToCreate = ({ workspaceId, createdByUserWorkspaceId, workspaceCustomApplicationId, + flatFieldMetadataMaps, }: { createViewInput: CreateViewInput; workspaceId: string; createdByUserWorkspaceId?: string; workspaceCustomApplicationId: string; -}): FlatView => { + flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps']; +}): { + flatViewToCreate: FlatView; + flatViewGroupsToCreate: FlatViewGroup[]; +} => { const { objectMetadataId, ...createViewInput } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawCreateViewInput, @@ -27,7 +38,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({ const createdAt = new Date(); const viewId = createViewInput.id ?? v4(); - return { + const flatViewToCreate = { id: viewId, objectMetadataId, workspaceId, @@ -58,4 +69,19 @@ export const fromCreateViewInputToFlatViewToCreate = ({ viewGroupIds: [], applicationId: workspaceCustomApplicationId, }; + + let flatViewGroupsToCreate: FlatViewGroup[] = []; + + if (isDefined(flatViewToCreate.mainGroupByFieldMetadataId)) { + flatViewGroupsToCreate = computeFlatViewGroupsOnViewCreate({ + flatViewToCreateId: flatViewToCreate.id, + mainGroupByFieldMetadataId: flatViewToCreate.mainGroupByFieldMetadataId, + flatFieldMetadataMaps, + }); + } + + return { + flatViewToCreate, + flatViewGroupsToCreate, + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-update-view-input-to-flat-view-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-update-view-input-to-flat-view-to-update-or-throw.util.ts index 66a2803da3..03bbbf1703 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-update-view-input-to-flat-view-to-update-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-update-view-input-to-flat-view-to-update-or-throw.util.ts @@ -5,9 +5,14 @@ import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type'; +import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; import { FLAT_VIEW_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view/constants/flat-view-editable-properties.constant'; import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type'; import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; +import { handleFlatViewUpdateSideEffect } from 'src/engine/metadata-modules/flat-view/utils/handle-flat-view-update-side-effect.util'; import { type UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input'; import { ViewException, @@ -18,10 +23,20 @@ import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing- export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({ updateViewInput: rawUpdateViewInput, flatViewMaps, + flatViewGroupMaps, + flatFieldMetadataMaps, + userWorkspaceId, }: { updateViewInput: UpdateViewInput; flatViewMaps: FlatViewMaps; -}): FlatView => { + flatViewGroupMaps: FlatViewGroupMaps; + flatFieldMetadataMaps: FlatEntityMaps; + userWorkspaceId?: string; +}): { + flatViewToUpdate: FlatView; + flatViewGroupsToDelete: FlatViewGroup[]; + flatViewGroupsToCreate: FlatViewGroup[]; +} => { const { id: viewToUpdateId } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawUpdateViewInput, @@ -42,9 +57,35 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({ FLAT_VIEW_EDITABLE_PROPERTIES, ); - return mergeUpdateInExistingRecord({ + const flatViewToUpdate = mergeUpdateInExistingRecord({ existing: existingFlatViewToUpdate, properties: FLAT_VIEW_EDITABLE_PROPERTIES, update: updatedEditableFieldProperties, }); + + // If changing visibility from WORKSPACE to UNLISTED, ensure createdByUserWorkspaceId is set + // This prevents the view from disappearing for the user making the change + if ( + isDefined(rawUpdateViewInput.visibility) && + rawUpdateViewInput.visibility === 'UNLISTED' && + existingFlatViewToUpdate.visibility === 'WORKSPACE' && + isDefined(userWorkspaceId) + ) { + // Re-allocate the view to the current user + flatViewToUpdate.createdByUserWorkspaceId = userWorkspaceId; + } + + const { flatViewGroupsToDelete, flatViewGroupsToCreate } = + handleFlatViewUpdateSideEffect({ + fromFlatView: existingFlatViewToUpdate, + toFlatView: flatViewToUpdate, + flatViewGroupMaps: flatViewGroupMaps, + flatFieldMetadataMaps: flatFieldMetadataMaps, + }); + + return { + flatViewToUpdate, + flatViewGroupsToDelete, + flatViewGroupsToCreate, + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/handle-flat-view-update-side-effect.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/handle-flat-view-update-side-effect.util.ts new file mode 100644 index 0000000000..99d870ba32 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/handle-flat-view-update-side-effect.util.ts @@ -0,0 +1,59 @@ +import { type FromTo } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +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 FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; +import { computeFlatViewGroupsOnViewCreate } from 'src/engine/metadata-modules/flat-view-group/utils/compute-flat-view-groups-on-view-create.util'; +import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; + +export type FlatViewUpdateSideEffects = { + flatViewGroupsToDelete: FlatViewGroup[]; + flatViewGroupsToCreate: FlatViewGroup[]; +}; + +type HandleFlatViewUpdateSideEffectArgs = FromTo & + Pick; + +export const FLAT_VIEW_UPDATE_EMPTY_SIDE_EFFECTS: FlatViewUpdateSideEffects = { + flatViewGroupsToDelete: [], + flatViewGroupsToCreate: [], +}; + +export const handleFlatViewUpdateSideEffect = ({ + fromFlatView, + toFlatView, + flatViewGroupMaps, + flatFieldMetadataMaps, +}: HandleFlatViewUpdateSideEffectArgs): FlatViewUpdateSideEffects => { + const sideEffectResult = structuredClone(FLAT_VIEW_UPDATE_EMPTY_SIDE_EFFECTS); + + const newMainGroupByFieldMetadataId = toFlatView.mainGroupByFieldMetadataId; + + const hasMainGroupByFieldMetadataIdChanged = + fromFlatView.mainGroupByFieldMetadataId !== newMainGroupByFieldMetadataId; + + if (!hasMainGroupByFieldMetadataIdChanged) { + return sideEffectResult; + } + + if (fromFlatView.viewGroupIds.length > 0) { + sideEffectResult.flatViewGroupsToDelete = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: fromFlatView.viewGroupIds, + flatEntityMaps: flatViewGroupMaps, + }); + } + + if (!isDefined(newMainGroupByFieldMetadataId)) { + return sideEffectResult; + } + + sideEffectResult.flatViewGroupsToCreate = computeFlatViewGroupsOnViewCreate({ + flatViewToCreateId: toFlatView.id, + mainGroupByFieldMetadataId: newMainGroupByFieldMetadataId, + flatFieldMetadataMaps, + }); + + return sideEffectResult; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index b2739ea1a6..6a6ca7f186 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -7,6 +7,7 @@ import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @@ -385,6 +386,7 @@ export class ObjectMetadataService extends TypeOrmQueryService UUIDScalarType, { nullable: true }) id?: string; - @IsUUID() - @Field(() => UUIDScalarType, { nullable: false }) - fieldMetadataId: string; - @IsOptional() @IsBoolean() @Field({ nullable: true, defaultValue: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/dtos/view-group.dto.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/dtos/view-group.dto.ts index 562c87c6ec..c5e6ccc4cc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/dtos/view-group.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/dtos/view-group.dto.ts @@ -9,9 +9,6 @@ export class ViewGroupDTO { @IDField(() => UUIDScalarType) id: string; - @Field(() => UUIDScalarType, { nullable: false }) - fieldMetadataId: string; - @Field({ nullable: false, defaultValue: true }) isVisible: boolean; diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/exceptions/view-group.exception.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/exceptions/view-group.exception.ts index 967caeaa2c..9a60a3684f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/exceptions/view-group.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/exceptions/view-group.exception.ts @@ -19,6 +19,7 @@ export enum ViewGroupExceptionCode { VIEW_GROUP_NOT_FOUND = 'VIEW_GROUP_NOT_FOUND', INVALID_VIEW_GROUP_DATA = 'INVALID_VIEW_GROUP_DATA', VIEW_NOT_FOUND = 'VIEW_NOT_FOUND', + MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID = 'MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID', } export enum ViewGroupExceptionMessageKey { diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/resolvers/view-group.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/resolvers/view-group.resolver.ts index f3f6b5f2b0..26c9c164fc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/resolvers/view-group.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/resolvers/view-group.resolver.ts @@ -11,14 +11,14 @@ import { DestroyViewGroupInput } from 'src/engine/metadata-modules/view-group/dt import { UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input'; import { ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto'; import { ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service'; +import { ViewGroupGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception.filter'; import { CreateViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-group-permission.guard'; import { DeleteViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-group-permission.guard'; import { DestroyViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-group-permission.guard'; import { UpdateViewGroupPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-group-permission.guard'; -import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter'; @Resolver(() => ViewGroupDTO) -@UseFilters(ViewGraphqlApiExceptionFilter) +@UseFilters(ViewGroupGraphqlApiExceptionFilter) @UseGuards(WorkspaceAuthGuard) export class ViewGroupResolver { constructor(private readonly viewGroupService: ViewGroupService) {} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group.service.ts index 893cf9d17e..e442f90cb6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group.service.ts @@ -92,12 +92,25 @@ export class ViewGroupService { ); const flatViewGroupsToCreate = createViewGroupInputs.map( - (createViewGroupInput) => - fromCreateViewGroupInputToFlatViewGroupToCreate({ + (createViewGroupInput) => { + const mainGroupByFieldMetadataId = + flatViewMaps.byId[createViewGroupInput.viewId] + ?.mainGroupByFieldMetadataId; + + if (!isDefined(mainGroupByFieldMetadataId)) { + throw new ViewGroupException( + 'The associated view is not a grouped view: mainGroupByFieldMetadataId is missing.', + ViewGroupExceptionCode.MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID, + ); + } + + return fromCreateViewGroupInputToFlatViewGroupToCreate({ createViewGroupInput, workspaceId, workspaceCustomApplicationId: workspaceCustomFlatApplication.id, - }), + mainGroupByFieldMetadataId, + }); + }, ); const validateAndBuildResult = diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception-handler.util.ts new file mode 100644 index 0000000000..73b4cd1cec --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception-handler.util.ts @@ -0,0 +1,26 @@ +import { type I18n } from '@lingui/core'; + +import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + ViewGroupException, + ViewGroupExceptionCode, +} from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception'; +import { viewGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util'; + +export const viewGroupGraphqlApiExceptionHandler = ( + error: Error, + i18n: I18n, +) => { + if (error instanceof ViewGroupException) { + if ( + error.code === + ViewGroupExceptionCode.MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID + ) { + throw new UserInputError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); + } + } + + return viewGraphqlApiExceptionHandler(error, i18n); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception.filter.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception.filter.ts new file mode 100644 index 0000000000..a9a782601a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception.filter.ts @@ -0,0 +1,32 @@ +import { + Catch, + type ExceptionFilter, + type ExecutionContext, + Injectable, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +import { SOURCE_LOCALE } from 'twenty-shared/translations'; + +import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; +import { ViewGroupException } from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception'; +import { viewGroupGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/view-group/utils/view-group-graphql-api-exception-handler.util'; +import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; + +@Catch(ViewGroupException, WorkspaceMigrationBuilderExceptionV2) +@Injectable() +export class ViewGroupGraphqlApiExceptionFilter implements ExceptionFilter { + constructor(private readonly i18nService: I18nService) {} + + catch( + exception: ViewGroupException | WorkspaceMigrationBuilderExceptionV2, + host: ExecutionContext, + ) { + const gqlContext = GqlExecutionContext.create(host); + const ctx = gqlContext.getContext(); + const userLocale = ctx.req?.locale ?? SOURCE_LOCALE; + const i18n = this.i18nService.getI18nInstance(userLocale); + + return viewGroupGraphqlApiExceptionHandler(exception, i18n); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/view-group.module.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/view-group.module.ts index ce2cf9ae34..d3164bbe62 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/view-group.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/view-group.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { ViewGroupController } from 'src/engine/metadata-modules/view-group/controllers/view-group.controller'; @@ -20,6 +21,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa WorkspaceCacheStorageModule, ApplicationModule, FeatureFlagModule, + I18nModule, PermissionsModule, WorkspaceMigrationV2Module, WorkspaceManyOrAllFlatEntityMapsCacheModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/dtos/inputs/update-view.input.ts b/packages/twenty-server/src/engine/metadata-modules/view/dtos/inputs/update-view.input.ts index f92d022f3e..f7b86a989b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/dtos/inputs/update-view.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/dtos/inputs/update-view.input.ts @@ -91,5 +91,5 @@ export class UpdateViewInput { @IsOptional() @IsUUID() @Field(() => UUIDScalarType, { nullable: true }) - mainGroupByFieldMetadataId?: string; + mainGroupByFieldMetadataId?: string | null; } diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts index 1dcdc2987a..ae11a40da2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; +import { isDefined } from 'twenty-shared/utils'; import { IsNull, Repository } from 'typeorm'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; @@ -56,6 +56,7 @@ export class ViewService { flatObjectMetadataMaps, flatViewMaps: existingFlatViewMaps, flatFieldMetadataMaps: existingFlatFieldMetadataMaps, + flatViewGroupMaps: existingFlatViewGroupMaps, } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, @@ -63,16 +64,19 @@ export class ViewService { 'flatObjectMetadataMaps', 'flatViewMaps', 'flatFieldMetadataMaps', + 'flatViewGroupMaps', ], }, ); - const flatViewFromCreateInput = fromCreateViewInputToFlatViewToCreate({ - createViewInput, - workspaceId, - createdByUserWorkspaceId, - workspaceCustomApplicationId: workspaceCustomFlatApplication.id, - }); + const { flatViewToCreate, flatViewGroupsToCreate } = + fromCreateViewInputToFlatViewToCreate({ + createViewInput, + workspaceId, + createdByUserWorkspaceId, + workspaceCustomApplicationId: workspaceCustomFlatApplication.id, + flatFieldMetadataMaps: existingFlatFieldMetadataMaps, + }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( @@ -80,7 +84,13 @@ export class ViewService { fromToAllFlatEntityMaps: { flatViewMaps: computeFlatEntityMapsFromTo({ flatEntityMaps: existingFlatViewMaps, - flatEntityToCreate: [flatViewFromCreateInput], + flatEntityToCreate: [flatViewToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), + flatViewGroupMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewGroupMaps, + flatEntityToCreate: flatViewGroupsToCreate, flatEntityToDelete: [], flatEntityToUpdate: [], }), @@ -112,7 +122,7 @@ export class ViewService { ); return findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityId: flatViewFromCreateInput.id, + flatEntityId: flatViewToCreate.id, flatEntityMaps: recomputedExistingFlatViewMaps, }); } @@ -129,35 +139,27 @@ export class ViewService { const { flatViewMaps: existingFlatViewMaps, flatFieldMetadataMaps: existingFlatFieldMetadataMaps, - } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatViewMaps', 'flatFieldMetadataMaps'], - }, - ); + flatViewGroupMaps: existingFlatViewGroupMaps, + } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatViewMaps', + 'flatFieldMetadataMaps', + 'flatViewGroupMaps', + ], + }, + ); - const flatViewFromUpdateInput = + const { flatViewToUpdate, flatViewGroupsToDelete, flatViewGroupsToCreate } = fromUpdateViewInputToFlatViewToUpdateOrThrow({ updateViewInput, flatViewMaps: existingFlatViewMaps, + flatViewGroupMaps: existingFlatViewGroupMaps, + flatFieldMetadataMaps: existingFlatFieldMetadataMaps, + userWorkspaceId, }); - const existingFlatView = existingFlatViewMaps.byId[updateViewInput.id]; - - // If changing visibility from WORKSPACE to UNLISTED, ensure createdByUserWorkspaceId is set - // This prevents the view from disappearing for the user making the change - if ( - isDefined(existingFlatView) && - isDefined(updateViewInput.visibility) && - updateViewInput.visibility === 'UNLISTED' && - existingFlatView.visibility === 'WORKSPACE' && - isDefined(userWorkspaceId) - ) { - // Re-allocate the view to the current user - flatViewFromUpdateInput.createdByUserWorkspaceId = userWorkspaceId; - } - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -166,7 +168,13 @@ export class ViewService { flatEntityMaps: existingFlatViewMaps, flatEntityToCreate: [], flatEntityToDelete: [], - flatEntityToUpdate: [flatViewFromUpdateInput], + flatEntityToUpdate: [flatViewToUpdate], + }), + flatViewGroupMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewGroupMaps, + flatEntityToCreate: flatViewGroupsToCreate, + flatEntityToDelete: flatViewGroupsToDelete, + flatEntityToUpdate: [], }), }, dependencyAllFlatEntityMaps: { @@ -174,6 +182,9 @@ export class ViewService { }, buildOptions: { isSystemBuild: false, + inferDeletionFromMissingEntities: { + viewGroup: true, + }, }, workspaceId, }, @@ -385,7 +396,6 @@ export class ViewService { if (view.visibility === ViewVisibility.WORKSPACE) { return true; } - if ( view.visibility === ViewVisibility.UNLISTED && isDefined(userWorkspaceId) && @@ -425,7 +435,6 @@ export class ViewService { if (view.visibility === ViewVisibility.WORKSPACE) { return true; } - if ( view.visibility === ViewVisibility.UNLISTED && isDefined(userWorkspaceId) && diff --git a/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util.ts index 6c241dfff3..553d36f4bb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util.ts @@ -3,6 +3,7 @@ import { assertUnreachable } from 'twenty-shared/utils'; import { ForbiddenError, + InternalServerError, NotFoundError, UserInputError, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; @@ -118,6 +119,10 @@ export const viewGraphqlApiExceptionHandler = (error: Error, i18n: I18n) => { throw new UserInputError(error.message, { userFriendlyMessage: error.userFriendlyMessage, }); + case ViewGroupExceptionCode.MISSING_MAIN_GROUP_BY_FIELD_METADATA_ID: + throw new InternalServerError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); default: { return assertUnreachable(error.code); } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 0e97a85775..22deb35ba3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -148,7 +148,6 @@ describe('WorkspaceEntityManager', () => { workspaceId: 'test-workspace-id', viewFieldIds: [], viewFilterIds: [], - viewGroupIds: [], kanbanAggregateOperationViewIds: [], calendarViewIds: [], mainGroupByFieldMetadataViewIds: [], diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index d271b4dc99..db1ab3c7e2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -91,7 +91,6 @@ describe('WorkspaceRepository', () => { relationTargetObjectMetadataId: null, calendarViewIds: [], viewFilterIds: [], - viewGroupIds: [], kanbanAggregateOperationViewIds: [], viewFieldIds: [], mainGroupByFieldMetadataViewIds: [], diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/create-standard-field-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/create-standard-field-flat-metadata.util.ts index f469975849..dbacdb457a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/create-standard-field-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/create-standard-field-flat-metadata.util.ts @@ -99,7 +99,6 @@ export const createStandardFieldFlatMetadata = < morphId: null, viewFieldIds: [], viewFilterIds: [], - viewGroupIds: [], kanbanAggregateOperationViewIds: [], calendarViewIds: [], mainGroupByFieldMetadataViewIds: [], diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/create-standard-relation-field-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/create-standard-relation-field-flat-metadata.util.ts index 3052cd0d01..3cb2693bff 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/create-standard-relation-field-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/create-standard-relation-field-flat-metadata.util.ts @@ -108,7 +108,6 @@ export const createStandardRelationFieldFlatMetadata = < morphId: null, viewFieldIds: [], viewFilterIds: [], - viewGroupIds: [], kanbanAggregateOperationViewIds: [], calendarViewIds: [], mainGroupByFieldMetadataViewIds: [], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/__tests__/__snapshots__/flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/__tests__/__snapshots__/flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap index b470ab2e96..02766a7f69 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/__tests__/__snapshots__/flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/__tests__/__snapshots__/flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap @@ -36,7 +36,6 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a crea "updatedAt": 2024-01-01T00:00:00.000Z, "viewFieldIds": [], "viewFilterIds": [], - "viewGroupIds": [], "workspaceId": "workspace-id-1", }, }, @@ -101,7 +100,6 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a dele "updatedAt": 2024-01-01T00:00:00.000Z, "viewFieldIds": [], "viewFilterIds": [], - "viewGroupIds": [], "workspaceId": "workspace-id-1", }, }, @@ -184,7 +182,6 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create "updatedAt": 2024-01-01T00:00:00.000Z, "viewFieldIds": [], "viewFilterIds": [], - "viewGroupIds": [], "workspaceId": "workspace-id-1", }, }, @@ -231,7 +228,6 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create "updatedAt": 2024-01-01T00:00:00.000Z, "viewFieldIds": [], "viewFilterIds": [], - "viewGroupIds": [], "workspaceId": "workspace-id-1", }, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts index 202972419d..1a0d8b2dc0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts @@ -20,7 +20,6 @@ export class FlatViewGroupValidatorService { optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { flatViewGroupMaps: optimisticFlatViewGroupMaps, flatViewMaps, - flatFieldMetadataMaps, }, }: FlatEntityUpdateValidationArgs< typeof ALL_METADATA_NAME.viewGroup @@ -53,10 +52,17 @@ export class FlatViewGroupValidatorService { }), }; + if (!isDefined(updatedFlatViewGroup.fieldValue)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Field value is required`, + userFriendlyMessage: msg`Field value is required`, + }); + } + validationResult.flatEntityMinimalInformation = { id: updatedFlatViewGroup.id, viewId: updatedFlatViewGroup.viewId, - fieldMetadataId: updatedFlatViewGroup.fieldMetadataId, }; const flatView = findFlatEntityByIdInFlatEntityMaps({ @@ -72,19 +78,6 @@ export class FlatViewGroupValidatorService { }); } - const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: updatedFlatViewGroup.fieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - }); - - if (!isDefined(flatFieldMetadata)) { - validationResult.errors.push({ - code: ViewExceptionCode.INVALID_VIEW_DATA, - message: t`View group to update parent field not found`, - userFriendlyMessage: msg`View group to update parent field not found`, - }); - } - return validationResult; } @@ -122,7 +115,6 @@ export class FlatViewGroupValidatorService { flatEntityToValidate: flatViewGroupToValidate, optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { flatViewGroupMaps: optimisticFlatViewGroupMaps, - flatFieldMetadataMaps, flatViewMaps, }, }: FlatEntityValidationArgs< @@ -134,7 +126,6 @@ export class FlatViewGroupValidatorService { flatEntityMinimalInformation: { id: flatViewGroupToValidate.id, viewId: flatViewGroupToValidate.viewId, - fieldMetadataId: flatViewGroupToValidate.fieldMetadataId, }, }; @@ -151,19 +142,6 @@ export class FlatViewGroupValidatorService { }); } - const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId: flatViewGroupToValidate.fieldMetadataId, - flatEntityMaps: flatFieldMetadataMaps, - }); - - if (!isDefined(flatFieldMetadata)) { - validationResult.errors.push({ - code: ViewExceptionCode.INVALID_VIEW_DATA, - message: t`Field metadata not found`, - userFriendlyMessage: msg`Field metadata not found`, - }); - } - const flatView = flatViewMaps.byId[flatViewGroupToValidate.viewId]; if (!isDefined(flatView)) { @@ -176,6 +154,14 @@ export class FlatViewGroupValidatorService { return validationResult; } + if (!isDefined(flatViewGroupToValidate.fieldValue)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Field value is required`, + userFriendlyMessage: msg`Field value is required`, + }); + } + return validationResult; } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service.ts index 910d19ed97..28451a9fbc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service.ts @@ -1,14 +1,17 @@ import { msg, t } from '@lingui/core/macro'; import { type ALL_METADATA_NAME } from 'twenty-shared/metadata'; +import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type'; +import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; import { ViewExceptionCode } from 'src/engine/metadata-modules/view/exceptions/view.exception'; import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util'; import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type'; import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type'; import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type'; +import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; export class FlatViewValidatorService { constructor() {} @@ -39,8 +42,19 @@ export class FlatViewValidatorService { message: t`View not found`, userFriendlyMessage: msg`View not found`, }); + + return validationResult; } + const partialUpdates = fromFlatEntityPropertiesUpdatesToPartialFlatEntity({ + updates: flatEntityUpdates, + }); + + const updatedFlatView: FlatView = { + ...existingFlatView, + ...partialUpdates, + }; + const kanbanAggregateOperationFieldMetadataIdUpdate = findFlatEntityPropertyUpdate({ property: 'kanbanAggregateOperationFieldMetadataId', @@ -64,6 +78,70 @@ export class FlatViewValidatorService { }); } + const viewBecomesKanban = + updatedFlatView.type === ViewType.KANBAN && + existingFlatView.type !== ViewType.KANBAN; + + if (viewBecomesKanban) { + if (!isDefined(updatedFlatView.mainGroupByFieldMetadataId)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Kanban view must have a main group by field`, + userFriendlyMessage: msg`Kanban view must have a main group by field`, + }); + + return validationResult; + } + + const mainGroupByFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: updatedFlatView.mainGroupByFieldMetadataId, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (!isDefined(mainGroupByFieldMetadata)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Kanban main group by field metadata not found`, + userFriendlyMessage: msg`Kanban main group by field metadata not found`, + }); + } else if (mainGroupByFieldMetadata.type !== FieldMetadataType.SELECT) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Kanban main group by field must be a SELECT field`, + userFriendlyMessage: msg`Kanban main group by field must be a select field`, + }); + } + } + + const updatedMainGroupByFieldMetadataId = + updatedFlatView.mainGroupByFieldMetadataId; + + const mainGroupByFieldMetadataIsAddedOrUpdated = + isDefined(updatedMainGroupByFieldMetadataId) && + existingFlatView.mainGroupByFieldMetadataId !== + updatedMainGroupByFieldMetadataId; + + if (mainGroupByFieldMetadataIsAddedOrUpdated && !viewBecomesKanban) { + const mainGroupByFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: updatedMainGroupByFieldMetadataId, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (!isDefined(mainGroupByFieldMetadata)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Kanban main group by field metadata not found`, + userFriendlyMessage: msg`Kanban main group by field metadata not found`, + }); + } else if (mainGroupByFieldMetadata.type !== FieldMetadataType.SELECT) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Kanban main group by field must be a SELECT field`, + userFriendlyMessage: msg`Kanban main group by field must be a select field`, + }); + } + } + return validationResult; } @@ -150,6 +228,39 @@ export class FlatViewValidatorService { }); } + const isKanban = flatViewToValidate.type === ViewType.KANBAN; + + if (isKanban) { + if (!isDefined(flatViewToValidate.mainGroupByFieldMetadataId)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Kanban view must have a main group by field`, + userFriendlyMessage: msg`Kanban view must have a main group by field`, + }); + + return validationResult; + } + + const mainGroupByFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: flatViewToValidate.mainGroupByFieldMetadataId, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (!isDefined(mainGroupByFieldMetadata)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Kanban main group by field metadata not found`, + userFriendlyMessage: msg`Kanban main group by field metadata not found`, + }); + } else if (mainGroupByFieldMetadata.type !== FieldMetadataType.SELECT) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Kanban main group by field must be a SELECT field`, + userFriendlyMessage: msg`Kanban main group by field must be a select field`, + }); + } + } + return validationResult; } } diff --git a/packages/twenty-server/test/integration/constants/view-gql-fields.constants.ts b/packages/twenty-server/test/integration/constants/view-gql-fields.constants.ts index 81e98b07a4..fd96c59106 100644 --- a/packages/twenty-server/test/integration/constants/view-gql-fields.constants.ts +++ b/packages/twenty-server/test/integration/constants/view-gql-fields.constants.ts @@ -4,6 +4,7 @@ export const VIEW_GQL_FIELDS = ` objectMetadataId type key + mainGroupByFieldMetadataId icon position isCompact @@ -50,7 +51,6 @@ export const VIEW_FILTER_GQL_FIELDS = ` export const VIEW_GROUP_GQL_FIELDS = ` id - fieldMetadataId fieldValue isVisible position diff --git a/packages/twenty-server/test/integration/graphql/suites/view/__snapshots__/successful-find-view.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/__snapshots__/successful-find-view.integration-spec.ts.snap index 20b919b7f8..b400c7184d 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/__snapshots__/successful-find-view.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/__snapshots__/successful-find-view.integration-spec.ts.snap @@ -9,6 +9,7 @@ exports[`successful find view with all sub-relations (e2e) Company View Structur "id": Any, "isCompact": false, "key": "INDEX", + "mainGroupByFieldMetadataId": null, "name": "All Companies", "objectMetadataId": Any, "openRecordIn": "SIDE_PANEL", diff --git a/packages/twenty-server/test/integration/graphql/suites/view/create-view.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/create-view.integration-spec.ts index 98d6b21bd3..43ffbd9f83 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/create-view.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/create-view.integration-spec.ts @@ -1,3 +1,4 @@ +import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; @@ -9,6 +10,7 @@ import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum' describe('Create core view', () => { let testObjectMetadataId: string; + let testSelectFieldMetadataId: string; beforeAll(async () => { const { @@ -27,6 +29,15 @@ describe('Create core view', () => { }); testObjectMetadataId = objectMetadataId; + + const { selectFieldMetadataId } = + await createOneSelectFieldMetadataForIntegrationTests({ + input: { + objectMetadataId, + }, + }); + + testSelectFieldMetadataId = selectFieldMetadataId; }); afterAll(async () => { @@ -52,6 +63,7 @@ describe('Create core view', () => { objectMetadataId: testObjectMetadataId, icon: 'IconDeal', type: ViewType.KANBAN, + mainGroupByFieldMetadataId: testSelectFieldMetadataId, position: 1, isCompact: true, openRecordIn: ViewOpenRecordIn.SIDE_PANEL, @@ -63,6 +75,7 @@ describe('Create core view', () => { assertViewStructure(data.createCoreView, { name: 'Kanban View', objectMetadataId: testObjectMetadataId, + mainGroupByFieldMetadataId: testSelectFieldMetadataId, type: ViewType.KANBAN, key: null, icon: 'IconDeal', @@ -90,6 +103,7 @@ describe('Create core view', () => { objectMetadataId: input.objectMetadataId, icon: input.icon, type: ViewType.TABLE, + mainGroupByFieldMetadataId: null, key: null, position: 0, isCompact: false, diff --git a/packages/twenty-server/test/integration/graphql/suites/view/update-view.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/update-view.integration-spec.ts index 15085a733d..af235ba819 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/update-view.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/update-view.integration-spec.ts @@ -1,4 +1,5 @@ import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; @@ -11,6 +12,7 @@ const TEST_NOT_EXISTING_VIEW_ID = '20202020-0000-4000-8000-000000000000'; describe('Update core view', () => { let testObjectMetadataId: string; + let testSelectFieldMetadataId: string; beforeAll(async () => { const { @@ -28,7 +30,31 @@ describe('Update core view', () => { }, }); + const { selectFieldMetadataId } = + await createOneSelectFieldMetadataForIntegrationTests({ + expectToFail: false, + input: { + objectMetadataId, + options: [ + { + label: 'Option 1', + value: 'OPTION_1', + color: 'blue', + position: 0, + }, + { label: 'Option 2', value: 'OPTION_2', color: 'red', position: 1 }, + { + label: 'Option 3', + value: 'OPTION_3', + color: 'green', + position: 2, + }, + ], + }, + }); + testObjectMetadataId = objectMetadataId; + testSelectFieldMetadataId = selectFieldMetadataId; }); afterAll(async () => { @@ -65,6 +91,7 @@ describe('Update core view', () => { id: view.id, name: 'Updated View', type: ViewType.KANBAN, + mainGroupByFieldMetadataId: testSelectFieldMetadataId, isCompact: true, }; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-group-resolver.integration-spec.ts index 4959c4f56f..544e641b2a 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-group-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-group-resolver.integration-spec.ts @@ -1,17 +1,15 @@ import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; import { createTestViewWithGraphQL } from 'test/integration/graphql/utils/view-graphql.util'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { createOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/create-one-core-view-group.util'; import { deleteOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/delete-one-core-view-group.util'; import { destroyOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/destroy-one-core-view-group.util'; -import { findCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/find-core-view-groups.util'; import { updateOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/update-one-core-view-group.util'; import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util'; import { assertViewGroupStructure } from 'test/integration/utils/view-test.util'; -import { FieldMetadataType } from 'twenty-shared/types'; const TEST_NOT_EXISTING_VIEW_GROUP_ID = '20202020-0000-4000-8000-000000000003'; @@ -38,22 +36,14 @@ describe('View Group Resolver', () => { testObjectMetadataId = objectMetadataId; - const { - data: { - createOneField: { id: fieldMetadataId }, - }, - } = await createOneFieldMetadata({ - expectToFail: false, - input: { - name: 'testField', - label: 'Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: testObjectMetadataId, - isLabelSyncedWithName: true, - }, - }); + const { selectFieldMetadataId } = + await createOneSelectFieldMetadataForIntegrationTests({ + input: { + objectMetadataId: testObjectMetadataId, + }, + }); - testFieldMetadataId = fieldMetadataId; + testFieldMetadataId = selectFieldMetadataId; }); afterAll(async () => { @@ -76,6 +66,7 @@ describe('View Group Resolver', () => { const view = await createTestViewWithGraphQL({ name: 'Test View for Groups', objectMetadataId: testObjectMetadataId, + mainGroupByFieldMetadataId: testFieldMetadataId, }); testViewId = view.id; @@ -88,51 +79,12 @@ describe('View Group Resolver', () => { }); }); - describe('getCoreViewGroups', () => { - it('should return empty array when no view groups exist', async () => { - const { data } = await findCoreViewGroups({ - viewId: testViewId, - expectToFail: false, - }); - - expect(data.getCoreViewGroups).toEqual([]); - }); - - it('should return view groups for a specific view', async () => { - await createOneCoreViewGroup({ - expectToFail: false, - input: { - viewId: testViewId, - fieldMetadataId: testFieldMetadataId, - isVisible: true, - fieldValue: 'active', - position: 0, - }, - }); - - const { data } = await findCoreViewGroups({ - viewId: testViewId, - expectToFail: false, - }); - - expect(data.getCoreViewGroups).toHaveLength(1); - assertViewGroupStructure(data.getCoreViewGroups[0], { - fieldMetadataId: testFieldMetadataId, - isVisible: true, - fieldValue: 'active', - position: 0, - viewId: testViewId, - }); - }); - }); - describe('createCoreViewGroup', () => { it('should create a new view group', async () => { const { data } = await createOneCoreViewGroup({ expectToFail: false, input: { viewId: testViewId, - fieldMetadataId: testFieldMetadataId, isVisible: false, fieldValue: 'inactive', position: 1, @@ -140,7 +92,6 @@ describe('View Group Resolver', () => { }); assertViewGroupStructure(data.createCoreViewGroup, { - fieldMetadataId: testFieldMetadataId, isVisible: false, fieldValue: 'inactive', position: 1, @@ -153,7 +104,6 @@ describe('View Group Resolver', () => { expectToFail: false, input: { viewId: testViewId, - fieldMetadataId: testFieldMetadataId, isVisible: true, fieldValue: '', position: 2, @@ -161,7 +111,6 @@ describe('View Group Resolver', () => { }); assertViewGroupStructure(data.createCoreViewGroup, { - fieldMetadataId: testFieldMetadataId, isVisible: true, fieldValue: '', position: 2, @@ -175,7 +124,6 @@ describe('View Group Resolver', () => { expectToFail: false, input: { viewId: testViewId, - fieldMetadataId: testFieldMetadataId, isVisible: true, fieldValue: 'original', position: 0, @@ -224,7 +172,6 @@ describe('View Group Resolver', () => { expectToFail: false, input: { viewId: testViewId, - fieldMetadataId: testFieldMetadataId, isVisible: true, fieldValue: 'to delete', position: 0, @@ -263,7 +210,6 @@ describe('View Group Resolver', () => { expectToFail: false, input: { viewId: testViewId, - fieldMetadataId: testFieldMetadataId, isVisible: true, fieldValue: 'to destroy', position: 0, diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap deleted file mode 100644 index bf15ab1db6..0000000000 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-group/__snapshots__/failing-create-many-view-groups-v2.integration-spec.ts.snap +++ /dev/null @@ -1,104 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`View Group Resolver - Failing Create Many Operations - v2 should accumulate multiple validation errors when some inputs are invalid 1`] = ` -{ - "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "agent": [], - "cronTrigger": [], - "databaseEventTrigger": [], - "fieldMetadata": [], - "index": [], - "objectMetadata": [], - "pageLayoutTab": [], - "pageLayoutWidget": [], - "role": [], - "roleTarget": [], - "routeTrigger": [], - "serverlessFunction": [], - "view": [], - "viewField": [], - "viewFilter": [], - "viewGroup": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_group", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_group", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_group", - }, - ], - }, - "message": "Validation failed for 0 object(s) and 0 field(s)", - "summary": { - "invalidAgent": 0, - "invalidCronTrigger": 0, - "invalidDatabaseEventTrigger": 0, - "invalidFieldMetadata": 0, - "invalidIndex": 0, - "invalidObjectMetadata": 0, - "invalidPageLayoutTab": 0, - "invalidPageLayoutWidget": 0, - "invalidRole": 0, - "invalidRoleTarget": 0, - "invalidRouteTrigger": 0, - "invalidServerlessFunction": 0, - "invalidView": 0, - "invalidViewField": 0, - "invalidViewFilter": 0, - "invalidViewGroup": 0, - "totalErrors": 0, - }, - "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", - }, - "message": "Multiple validation errors occurred while creating view groups", - "name": "GraphQLError", -} -`; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group/failing-create-many-view-groups-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-group/failing-create-many-view-groups-v2.integration-spec.ts deleted file mode 100644 index 5a817b5cf5..0000000000 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-group/failing-create-many-view-groups-v2.integration-spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { createManyCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/create-many-core-view-groups.util'; -import { v4 as uuidv4 } from 'uuid'; -import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input'; - -describe('View Group Resolver - Failing Create Many Operations - v2', () => { - let testSetup: { - testViewId: string; - testObjectMetadataId: string; - firstTestFieldMetadataId: string; - secondTestFieldMetadataId: string; - }; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - expectToFail: false, - input: { - nameSingular: 'myGroupTestObjectV2', - namePlural: 'myGroupTestObjectsV2', - labelSingular: 'My Group Test Object v2', - labelPlural: 'My Group Test Objects v2', - icon: 'Icon123', - }, - }); - - const { - data: { - createOneField: { id: firstTestFieldMetadataId }, - }, - } = await createOneFieldMetadata({ - expectToFail: false, - input: { - name: 'testField', - label: 'Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId, - isLabelSyncedWithName: true, - }, - gqlFields: ` - id - name - label - isLabelSyncedWithName - `, - }); - - const { - data: { - createOneField: { id: secondTestFieldMetadataId }, - }, - } = await createOneFieldMetadata({ - expectToFail: false, - input: { - name: 'secondTestField', - label: 'Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId, - isLabelSyncedWithName: false, - }, - gqlFields: ` - id - name - label - isLabelSyncedWithName - `, - }); - - const { - data: { - createCoreView: { id: testViewId }, - }, - } = await createOneCoreView({ - input: { - icon: 'icon123', - objectMetadataId, - name: 'TestViewForGroups', - }, - expectToFail: false, - }); - - testSetup = { - testViewId, - testObjectMetadataId: objectMetadataId, - firstTestFieldMetadataId, - secondTestFieldMetadataId, - }; - }); - - afterAll(async () => { - await updateOneObjectMetadata({ - input: { - idToUpdate: testSetup.testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - expectToFail: false, - input: { idToDelete: testSetup.testObjectMetadataId }, - }); - }); - - it('should accumulate multiple validation errors when some inputs are invalid', async () => { - const invalidViewId = uuidv4(); - const invalidFieldMetadataId = uuidv4(); - - const inputs: CreateViewGroupInput[] = [ - { - fieldMetadataId: invalidFieldMetadataId, - viewId: testSetup.testViewId, - position: 0, - isVisible: true, - fieldValue: 'Invalid Group A', - }, - { - fieldMetadataId: testSetup.firstTestFieldMetadataId, - viewId: invalidViewId, - position: 1, - isVisible: true, - fieldValue: 'Invalid Group B', - }, - { - fieldMetadataId: invalidFieldMetadataId, - viewId: invalidViewId, - position: 2, - isVisible: true, - fieldValue: 'Invalid Group C', - }, - ]; - - const { errors } = await createManyCoreViewGroups({ - inputs, - expectToFail: true, - }); - - expectOneNotInternalServerErrorSnapshot({ - errors, - }); - }); -}); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group/successful-create-many-view-groups-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-group/successful-create-many-view-groups-v2.integration-spec.ts index fc9cf21149..8c9b4761a2 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-group/successful-create-many-view-groups-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-group/successful-create-many-view-groups-v2.integration-spec.ts @@ -1,4 +1,5 @@ import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; @@ -100,6 +101,13 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => { `, }); + const { selectFieldMetadataId } = + await createOneSelectFieldMetadataForIntegrationTests({ + input: { + objectMetadataId, + }, + }); + const { data: { createCoreView: { id: testViewId }, @@ -109,6 +117,7 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => { icon: 'icon123', objectMetadataId, name: 'TestViewForGroups', + mainGroupByFieldMetadataId: selectFieldMetadataId, }, expectToFail: false, }); @@ -164,21 +173,18 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => { it('should successfully create multiple view groups in batch', async () => { const inputs: CreateViewGroupInput[] = [ { - fieldMetadataId: testSetup.firstTestFieldMetadataId, viewId: testSetup.testViewId, position: 0, isVisible: true, fieldValue: 'Group A', }, { - fieldMetadataId: testSetup.secondTestFieldMetadataId, viewId: testSetup.testViewId, position: 1, isVisible: false, fieldValue: 'Group B', }, { - fieldMetadataId: testSetup.thirdTestFieldMetadataId, viewId: testSetup.testViewId, position: 2, isVisible: true, @@ -200,7 +206,6 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => { createdViewGroups.forEach((viewGroup, index) => { expect(viewGroup).toMatchObject({ - fieldMetadataId: inputs[index].fieldMetadataId, viewId: testSetup.testViewId, position: inputs[index].position, isVisible: inputs[index].isVisible, @@ -214,7 +219,6 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => { it('should successfully create single view group using batch endpoint', async () => { const inputs: CreateViewGroupInput[] = [ { - fieldMetadataId: testSetup.firstTestFieldMetadataId, viewId: testSetup.testViewId, position: 5, isVisible: true, @@ -237,7 +241,6 @@ describe('View Group Resolver - Successful Create Many Operations - v2', () => { const viewGroup = createdViewGroups[0]; expect(viewGroup).toMatchObject({ - fieldMetadataId: testSetup.firstTestFieldMetadataId, viewId: testSetup.testViewId, position: 5, isVisible: true, diff --git a/packages/twenty-server/test/integration/graphql/utils/view-graphql.util.ts b/packages/twenty-server/test/integration/graphql/utils/view-graphql.util.ts index 5b73124615..810eb17b3c 100644 --- a/packages/twenty-server/test/integration/graphql/utils/view-graphql.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/view-graphql.util.ts @@ -15,6 +15,8 @@ export const createTestViewWithGraphQL = async ( icon: viewData.icon, type: viewData.type, position: viewData.position, + mainGroupByFieldMetadataId: + viewData.mainGroupByFieldMetadataId ?? undefined, isCompact: viewData.isCompact, openRecordIn: viewData.openRecordIn, visibility: viewData.visibility, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/calendar-field-deactivation-deletes-views.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/calendar-field-deactivation-deletes-views.integration-spec.ts index 13c80f1f08..b99bf2b8b5 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/calendar-field-deactivation-deletes-views.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/calendar-field-deactivation-deletes-views.integration-spec.ts @@ -1,4 +1,5 @@ import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util'; import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; @@ -271,12 +272,21 @@ describe('calendar-field-deactivation-deletes-views', () => { }); it('should delete calendar view but not other view types when calendar field is deactivated', async () => { + const { selectFieldMetadataId } = + await createOneSelectFieldMetadataForIntegrationTests({ + input: { + objectMetadataId: testSetup.objectMetadataId, + name: 'selectField', + }, + }); + const { data: { createCoreView: kanbanViewWithSameObject }, } = await createOneCoreView({ input: { name: generateRecordName('Kanban View'), objectMetadataId: testSetup.objectMetadataId, + mainGroupByFieldMetadataId: selectFieldMetadataId, type: ViewType.KANBAN, icon: 'IconLayoutKanban', }, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/view-group-field-deactivation-deletes-views.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/field-group-by-deactivation-deletes-views.integration-spec.ts similarity index 71% rename from packages/twenty-server/test/integration/metadata/suites/field-metadata/view-group-field-deactivation-deletes-views.integration-spec.ts rename to packages/twenty-server/test/integration/metadata/suites/field-metadata/field-group-by-deactivation-deletes-views.integration-spec.ts index f12a1c28a2..4fd0e8486d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/view-group-field-deactivation-deletes-views.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/field-group-by-deactivation-deletes-views.integration-spec.ts @@ -3,7 +3,6 @@ import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-m import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { createOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/create-one-core-view-group.util'; import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; import { findOneCoreView } from 'test/integration/metadata/suites/view/utils/find-one-core-view.util'; import { generateRecordName } from 'test/integration/utils/generate-record-name'; @@ -29,7 +28,7 @@ type TestSetup = { viewWithoutGroupId: string; }; -describe('view-group-field-deactivation-deletes-views', () => { +describe('field-for-group-by-deactivation-deletes-views', () => { let testSetup: TestSetup; const verifyViewExists = async (viewId: string, shouldExist: boolean) => { @@ -126,22 +125,13 @@ describe('view-group-field-deactivation-deletes-views', () => { name: generateRecordName('View With Group'), objectMetadataId, type: ViewType.TABLE, + mainGroupByFieldMetadataId: groupByFieldMetadataId, icon: 'IconTable', }, gqlFields: VIEW_FIELDS, expectToFail: false, }); - await createOneCoreViewGroup({ - input: { - fieldMetadataId: groupByFieldMetadataId, - fieldValue: 'OPTION_1', - viewId: viewWithGroup.id, - }, - gqlFields: 'id', - expectToFail: false, - }); - const { data: { createCoreView: viewWithoutGroup }, } = await createOneCoreView({ @@ -180,7 +170,7 @@ describe('view-group-field-deactivation-deletes-views', () => { }); }); - it('should delete view when field used in view group is deactivated', async () => { + it('should delete view when mainGroupByFieldMetadata is deactivated', async () => { await verifyViewExists(testSetup.viewWithGroupId, true); await verifyViewExists(testSetup.viewWithoutGroupId, true); @@ -208,22 +198,13 @@ describe('view-group-field-deactivation-deletes-views', () => { name: generateRecordName('Second View With Group'), objectMetadataId: testSetup.objectMetadataId, type: ViewType.TABLE, + mainGroupByFieldMetadataId: testSetup.groupByFieldMetadataId, icon: 'IconTable', }, gqlFields: VIEW_FIELDS, expectToFail: false, }); - await createOneCoreViewGroup({ - input: { - fieldMetadataId: testSetup.groupByFieldMetadataId, - fieldValue: 'OPTION_2', - viewId: secondViewWithGroup.id, - }, - gqlFields: 'id', - expectToFail: false, - }); - await verifyViewExists(testSetup.viewWithGroupId, true); await verifyViewExists(secondViewWithGroup.id, true); await verifyViewExists(testSetup.viewWithoutGroupId, true); @@ -234,74 +215,4 @@ describe('view-group-field-deactivation-deletes-views', () => { await verifyViewExists(secondViewWithGroup.id, false); await verifyViewExists(testSetup.viewWithoutGroupId, true); }); - - it('should handle deactivation when view has multiple view groups with different fields', async () => { - const { - data: { - createOneField: { id: anotherGroupByFieldMetadataId }, - }, - } = await createOneFieldMetadata({ - expectToFail: false, - input: { - name: 'anotherGroupByField', - type: FieldMetadataType.SELECT, - label: 'Another Group By Field', - objectMetadataId: testSetup.objectMetadataId, - options: [ - { - label: 'Status A', - value: 'STATUS_A', - color: 'purple', - position: 0, - }, - { - label: 'Status B', - value: 'STATUS_B', - color: 'orange', - position: 1, - }, - ], - }, - gqlFields: 'id', - }); - - const { - data: { createCoreView: viewWithMultipleGroups }, - } = await createOneCoreView({ - input: { - name: generateRecordName('View With Multiple Groups'), - objectMetadataId: testSetup.objectMetadataId, - type: ViewType.TABLE, - icon: 'IconTable', - }, - gqlFields: VIEW_FIELDS, - expectToFail: false, - }); - - await createOneCoreViewGroup({ - input: { - fieldMetadataId: testSetup.groupByFieldMetadataId, - fieldValue: 'OPTION_1', - viewId: viewWithMultipleGroups.id, - }, - gqlFields: 'id', - expectToFail: false, - }); - - await createOneCoreViewGroup({ - input: { - fieldMetadataId: anotherGroupByFieldMetadataId, - fieldValue: 'STATUS_A', - viewId: viewWithMultipleGroups.id, - }, - gqlFields: 'id', - expectToFail: false, - }); - - await verifyViewExists(viewWithMultipleGroups.id, true); - - await deactivateFieldAndVerify(testSetup.groupByFieldMetadataId); - - await verifyViewExists(viewWithMultipleGroups.id, false); - }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/kanban-aggregate-field-deactivation-nullifies-views-kanban-properties.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/kanban-aggregate-field-deactivation-nullifies-views-kanban-properties.integration-spec.ts index 3cb63f223f..6f59907bcf 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/kanban-aggregate-field-deactivation-nullifies-views-kanban-properties.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/kanban-aggregate-field-deactivation-nullifies-views-kanban-properties.integration-spec.ts @@ -1,4 +1,5 @@ import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util'; import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; @@ -28,6 +29,7 @@ type TestSetup = { objectMetadataId: string; aggregateFieldMetadataId: string; nonAggregateFieldMetadataId: string; + selectFieldMetadataId: string; viewWithAggregateId: string; viewWithoutAggregateId: string; }; @@ -115,6 +117,29 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () = gqlFields: 'id', }); + const { selectFieldMetadataId } = + await createOneSelectFieldMetadataForIntegrationTests({ + input: { + name: 'selectField', + objectMetadataId, + options: [ + { + label: 'Option 1', + value: 'OPTION_1', + color: 'blue', + position: 0, + }, + { label: 'Option 2', value: 'OPTION_2', color: 'red', position: 1 }, + { + label: 'Option 3', + value: 'OPTION_3', + color: 'green', + position: 2, + }, + ], + }, + }); + const { data: { createCoreView: viewWithAggregate }, } = await createOneCoreView({ @@ -122,6 +147,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () = name: generateRecordName('Kanban View With Aggregate'), objectMetadataId, type: ViewType.KANBAN, + mainGroupByFieldMetadataId: selectFieldMetadataId, kanbanAggregateOperationFieldMetadataId: aggregateFieldMetadataId, kanbanAggregateOperation: AggregateOperations.SUM, icon: 'IconLayoutKanban', @@ -136,6 +162,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () = input: { name: generateRecordName('Kanban View Without Aggregate'), objectMetadataId, + mainGroupByFieldMetadataId: selectFieldMetadataId, type: ViewType.KANBAN, icon: 'IconLayoutKanban', }, @@ -147,6 +174,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () = objectMetadataId, aggregateFieldMetadataId, nonAggregateFieldMetadataId, + selectFieldMetadataId, viewWithAggregateId: viewWithAggregate.id, viewWithoutAggregateId: viewWithoutAggregate.id, }; @@ -253,6 +281,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () = name: generateRecordName('Second Kanban View With Aggregate'), objectMetadataId: testSetup.objectMetadataId, type: ViewType.KANBAN, + mainGroupByFieldMetadataId: testSetup.selectFieldMetadataId, kanbanAggregateOperationFieldMetadataId: testSetup.aggregateFieldMetadataId, kanbanAggregateOperation: AggregateOperations.MAX, @@ -318,6 +347,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () = name: generateRecordName('Kanban View With MIN'), objectMetadataId: testSetup.objectMetadataId, type: ViewType.KANBAN, + mainGroupByFieldMetadataId: testSetup.selectFieldMetadataId, kanbanAggregateOperationFieldMetadataId: testSetup.aggregateFieldMetadataId, kanbanAggregateOperation: AggregateOperations.MIN, @@ -334,6 +364,7 @@ describe('kanban-aggregate-field-deactivation-nullifies-kanban-properties', () = name: generateRecordName('Kanban View With AVG'), objectMetadataId: testSetup.objectMetadataId, type: ViewType.KANBAN, + mainGroupByFieldMetadataId: testSetup.selectFieldMetadataId, kanbanAggregateOperationFieldMetadataId: testSetup.aggregateFieldMetadataId, kanbanAggregateOperation: AggregateOperations.AVG, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-groups-side-effect-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-groups-side-effect-v2.integration-spec.ts index 15343fa785..d6e886d191 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-groups-side-effect-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-groups-side-effect-v2.integration-spec.ts @@ -5,13 +5,9 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { createOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/create-one-core-view-group.util'; import { findCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/find-core-view-groups.util'; import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; -import { - FieldMetadataType, - type EnumFieldMetadataType, -} from 'twenty-shared/types'; +import { FieldMetadataType } from 'twenty-shared/types'; import { type FieldMetadataComplexOption, @@ -37,16 +33,10 @@ const fakeOptionUpdate = ({ value, label, ...option }: Option) => ({ label: `${label} updated`, }); -const testFieldMetadataTypes: EnumFieldMetadataType[] = [ - FieldMetadataType.SELECT, - FieldMetadataType.MULTI_SELECT, -]; - describe('update-one-field-metadata-view-groups-side-effect-v2', () => { let objectMetadataIdToDelete: string; const createObjectWithSelectFieldAndView = async ( - fieldType: EnumFieldMetadataType, initialOptions: Option[], ) => { const singular = 'sideEffect'; @@ -69,11 +59,11 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => { const { data: { createOneField }, - } = await createOneFieldMetadata({ + } = await createOneFieldMetadata({ expectToFail: false, input: { objectMetadataId: createOneObject.id, - type: fieldType, + type: FieldMetadataType.SELECT, name: 'statusField', label: 'Status Field', isLabelSyncedWithName: true, @@ -90,6 +80,7 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => { icon: 'IconTable', name: 'Test View', objectMetadataId: createOneObject.id, + mainGroupByFieldMetadataId: createOneField.id, type: ViewType.TABLE, }, expectToFail: false, @@ -120,38 +111,30 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => { }); }); - describe.each(testFieldMetadataTypes)('%s field type', (fieldType) => { - it('should delete all view groups when all enum field options are deleted', async () => { + describe('SELECT field type', () => { + it('deleted and adds view groups when options are deleted and added', async () => { const initialOptions = generateOptions(3); const { fieldMetadataId, viewId } = - await createObjectWithSelectFieldAndView(fieldType, initialOptions); - - for (const [index, option] of initialOptions.entries()) { - await createOneCoreViewGroup({ - input: { - viewId, - fieldMetadataId, - fieldValue: option.value, - isVisible: true, - position: index, - }, - expectToFail: false, - gqlFields: 'id fieldValue', - }); - } + await createObjectWithSelectFieldAndView(initialOptions); const { data: { getCoreViewGroups: initialViewGroups }, } = await findCoreViewGroups({ viewId, - gqlFields: 'id fieldValue fieldMetadataId', + gqlFields: 'id fieldValue', expectToFail: false, }); - expect( - initialViewGroups.filter((vg) => vg.fieldMetadataId === fieldMetadataId) - .length, - ).toBe(3); + expect(initialViewGroups.length).toBe(4); + const initialFieldValues = [...initialViewGroups] + .map((vg) => vg.fieldValue) + .sort(); + const expectedFieldValues = [ + ...initialOptions.map((opt) => opt.value), + '', + ].sort(); + + expect(initialFieldValues).toEqual(expectedFieldValues); await updateOneFieldMetadata({ input: { @@ -175,82 +158,22 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => { data: { getCoreViewGroups: updatedViewGroups }, } = await findCoreViewGroups({ viewId, - gqlFields: 'id fieldValue fieldMetadataId', + gqlFields: 'id fieldValue viewId', expectToFail: false, }); - expect( - updatedViewGroups.filter((vg) => vg.fieldMetadataId === fieldMetadataId) - .length, - ).toBe(0); + expect(updatedViewGroups.length).toBe(2); + const updatedFieldValues = updatedViewGroups + .map((vg) => vg.fieldValue) + .sort(); + const expectedUpdatedFieldValues = ['', 'NEW_OPTION_VALUE'].sort(); + + expect(updatedFieldValues).toEqual(expectedUpdatedFieldValues); }); - - it('should update view group when option value is updated', async () => { - const initialOptions = generateOptions(1); - const { fieldMetadataId, fieldOptions, viewId } = - await createObjectWithSelectFieldAndView(fieldType, initialOptions); - - await createOneCoreViewGroup({ - input: { - viewId, - fieldMetadataId, - fieldValue: initialOptions[0].value, - isVisible: true, - position: 0, - }, - expectToFail: false, - gqlFields: 'id fieldValue', - }); - - const updatedOptions = [fakeOptionUpdate(fieldOptions[0])]; - - await updateOneFieldMetadata({ - input: { - idToUpdate: fieldMetadataId, - updatePayload: { - options: updatedOptions, - }, - }, - gqlFields: 'id options', - expectToFail: false, - }); - - const { - data: { getCoreViewGroups: updatedViewGroups }, - } = await findCoreViewGroups({ - viewId, - gqlFields: 'id fieldValue fieldMetadataId', - expectToFail: false, - }); - - const viewGroupsForField = updatedViewGroups.filter( - (vg) => vg.fieldMetadataId === fieldMetadataId, - ); - - expect(viewGroupsForField.length).toBe(1); - expect(viewGroupsForField[0].fieldValue).toBe( - `${initialOptions[0].value}_UPDATED`, - ); - }); - - it('should update multiple view groups when multiple option values are updated', async () => { + it('updates specific view groups when some options are updated', async () => { const initialOptions = generateOptions(3); const { fieldMetadataId, fieldOptions, viewId } = - await createObjectWithSelectFieldAndView(fieldType, initialOptions); - - for (const [index, option] of initialOptions.entries()) { - await createOneCoreViewGroup({ - input: { - viewId, - fieldMetadataId, - fieldValue: option.value, - isVisible: true, - position: index, - }, - expectToFail: false, - gqlFields: 'id fieldValue', - }); - } + await createObjectWithSelectFieldAndView(initialOptions); const updatedOptions = fieldOptions.map((opt) => fakeOptionUpdate(opt)); @@ -269,43 +192,26 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => { data: { getCoreViewGroups: updatedViewGroups }, } = await findCoreViewGroups({ viewId, - gqlFields: 'id fieldValue fieldMetadataId', + gqlFields: 'id fieldValue', expectToFail: false, }); - const viewGroupsForField = updatedViewGroups.filter( - (vg) => vg.fieldMetadataId === fieldMetadataId, - ); - - expect(viewGroupsForField.length).toBe(3); - const actualFieldValues = viewGroupsForField + expect(updatedViewGroups.length).toBe(4); + const updatedFieldValues = updatedViewGroups .map((vg) => vg.fieldValue) .sort(); - const expectedFieldValues = initialOptions - .map((opt) => `${opt.value}_UPDATED`) - .sort(); + const expectedUpdatedFieldValues = [ + ...updatedOptions.map((opt) => opt.value), + '', + ].sort(); - expect(actualFieldValues).toEqual(expectedFieldValues); + expect(updatedFieldValues).toEqual(expectedUpdatedFieldValues); }); - it('should delete specific view groups when their options are removed', async () => { + it('deletes specific view groups when some of all options are removed', async () => { const initialOptions = generateOptions(5); const { fieldMetadataId, fieldOptions, viewId } = - await createObjectWithSelectFieldAndView(fieldType, initialOptions); - - for (const [index, option] of initialOptions.entries()) { - await createOneCoreViewGroup({ - input: { - viewId, - fieldMetadataId, - fieldValue: option.value, - isVisible: true, - position: index, - }, - expectToFail: false, - gqlFields: 'id fieldValue', - }); - } + await createObjectWithSelectFieldAndView(initialOptions); const updatedOptions = fieldOptions.slice(2); @@ -324,44 +230,26 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => { data: { getCoreViewGroups: updatedViewGroups }, } = await findCoreViewGroups({ viewId, - gqlFields: 'id fieldValue fieldMetadataId', + gqlFields: 'id fieldValue', expectToFail: false, }); - const viewGroupsForField = updatedViewGroups.filter( - (vg) => vg.fieldMetadataId === fieldMetadataId, - ); + expect(updatedViewGroups.length).toBe(4); + const remainingFieldValues = updatedViewGroups + .map((vg) => vg.fieldValue) + .sort(); + const expectedRemainingFieldValues = [ + ...updatedOptions.map((opt) => opt.value), + '', + ].sort(); - expect(viewGroupsForField.length).toBe(3); - const remainingFieldValues = viewGroupsForField.map( - (vg) => vg.fieldValue, - ); - - expect(remainingFieldValues).not.toContain(initialOptions[0].value); - expect(remainingFieldValues).not.toContain(initialOptions[1].value); - expect(remainingFieldValues).toContain(initialOptions[2].value); - expect(remainingFieldValues).toContain(initialOptions[3].value); - expect(remainingFieldValues).toContain(initialOptions[4].value); + expect(remainingFieldValues).toEqual(expectedRemainingFieldValues); }); - it('should preserve view groups when no options are changed', async () => { + it('preserves view groups when no options are changed', async () => { const initialOptions = generateOptions(3); const { fieldMetadataId, fieldOptions, viewId } = - await createObjectWithSelectFieldAndView(fieldType, initialOptions); - - for (const [index, option] of initialOptions.entries()) { - await createOneCoreViewGroup({ - input: { - viewId, - fieldMetadataId, - fieldValue: option.value, - isVisible: true, - position: index, - }, - expectToFail: false, - gqlFields: 'id fieldValue', - }); - } + await createObjectWithSelectFieldAndView(initialOptions); await updateOneFieldMetadata({ input: { @@ -378,41 +266,26 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => { data: { getCoreViewGroups: updatedViewGroups }, } = await findCoreViewGroups({ viewId, - gqlFields: 'id fieldValue fieldMetadataId', + gqlFields: 'id fieldValue', expectToFail: false, }); - const viewGroupsForField = updatedViewGroups.filter( - (vg) => vg.fieldMetadataId === fieldMetadataId, - ); - - expect(viewGroupsForField.length).toBe(3); - const actualFieldValues = viewGroupsForField + expect(updatedViewGroups.length).toBe(4); + const actualFieldValues = updatedViewGroups .map((vg) => vg.fieldValue) .sort(); - const expectedFieldValues = initialOptions.map((opt) => opt.value).sort(); + const expectedFieldValues = [ + ...initialOptions.map((opt) => opt.value), + '', + ].sort(); expect(actualFieldValues).toEqual(expectedFieldValues); }); - it('should handle adding new options while maintaining existing view groups', async () => { + it('adds new view groups when new options are added', async () => { const initialOptions = generateOptions(3); const { fieldMetadataId, fieldOptions, viewId } = - await createObjectWithSelectFieldAndView(fieldType, initialOptions); - - for (const [index, option] of initialOptions.entries()) { - await createOneCoreViewGroup({ - input: { - viewId, - fieldMetadataId, - fieldValue: option.value, - isVisible: true, - position: index, - }, - expectToFail: false, - gqlFields: 'id fieldValue', - }); - } + await createObjectWithSelectFieldAndView(initialOptions); const newOptions = generateOptions(6).slice(3); const updatedOptions = [...fieldOptions, ...newOptions]; @@ -432,19 +305,18 @@ describe('update-one-field-metadata-view-groups-side-effect-v2', () => { data: { getCoreViewGroups: updatedViewGroups }, } = await findCoreViewGroups({ viewId, - gqlFields: 'id fieldValue fieldMetadataId', + gqlFields: 'id fieldValue', expectToFail: false, }); - const viewGroupsForField = updatedViewGroups.filter( - (vg) => vg.fieldMetadataId === fieldMetadataId, - ); - - expect(viewGroupsForField.length).toBe(6); - const actualFieldValues = viewGroupsForField + expect(updatedViewGroups.length).toBe(7); + const actualFieldValues = updatedViewGroups .map((vg) => vg.fieldValue) .sort(); - const expectedFieldValues = updatedOptions.map((opt) => opt.value).sort(); + const expectedFieldValues = [ + ...updatedOptions.map((opt) => opt.value), + '', + ].sort(); expect(actualFieldValues).toEqual(expectedFieldValues); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util.ts new file mode 100644 index 0000000000..d9a8247d05 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util.ts @@ -0,0 +1,41 @@ +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { + type FieldMetadataComplexOption, + FieldMetadataType, +} from 'twenty-shared/types'; + +import { type CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; + +export const createOneSelectFieldMetadataForIntegrationTests = async ({ + input, + expectToFail = false, +}: { + input: Partial & { + objectMetadataId: string; + options?: FieldMetadataComplexOption[]; + name?: string; + }; + expectToFail?: boolean; +}) => { + const { + data: { + createOneField: { id: selectFieldMetadataId }, + }, + } = await createOneFieldMetadata({ + expectToFail, + input: { + name: 'normalField', + type: FieldMetadataType.SELECT, + label: 'Select Field', + options: [ + { label: 'Option 1', value: 'OPTION_1', color: 'blue', position: 0 }, + { label: 'Option 2', value: 'OPTION_2', color: 'red', position: 1 }, + { label: 'Option 3', value: 'OPTION_3', color: 'green', position: 2 }, + ], + ...input, + }, + gqlFields: 'id', + }); + + return { selectFieldMetadataId }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view/update-one-view-view-groups-side-effect-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view/update-one-view-view-groups-side-effect-v2.integration-spec.ts new file mode 100644 index 0000000000..ba1202b0e9 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view/update-one-view-view-groups-side-effect-v2.integration-spec.ts @@ -0,0 +1,285 @@ +import { faker } from '@faker-js/faker'; +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { findCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/find-core-view-groups.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { updateOneCoreView } from 'test/integration/metadata/suites/view/utils/update-one-core-view.util'; +import { + FieldMetadataType, + type EnumFieldMetadataType, +} from 'twenty-shared/types'; + +import { + type FieldMetadataComplexOption, + type FieldMetadataDefaultOption, +} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; + +type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption; + +const generateOption = (index: number): Option => ({ + label: `Option ${index}`, + value: `OPTION_${index}`, + color: 'green', + position: index, +}); + +const generateOptions = (length: number) => + Array.from({ length }, (_value, index) => generateOption(index)); + +const testFieldMetadataTypes: EnumFieldMetadataType[] = [ + FieldMetadataType.SELECT, + FieldMetadataType.MULTI_SELECT, +]; + +describe('update-one-view-view-groups-side-effect-v2', () => { + let objectMetadataIdToDelete: string; + + const createObjectWithTwoSelectFieldsAndKanbanViewAndGroupedTableView = + async (initialOptions: Option[]) => { + const singular = 'viewSideEffect'; + const plural = 'viewSideEffects'; + + const { + data: { createOneObject }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: getMockCreateObjectInput({ + labelSingular: singular, + labelPlural: plural, + nameSingular: singular, + namePlural: plural, + isLabelSyncedWithName: false, + }), + }); + + objectMetadataIdToDelete = createOneObject.id; + + const { + data: { createOneField: createOneStatusField }, + } = await createOneFieldMetadata({ + expectToFail: false, + input: { + objectMetadataId: createOneObject.id, + type: FieldMetadataType.SELECT, + name: 'statusField', + label: 'Status Field', + isLabelSyncedWithName: true, + options: initialOptions, + }, + gqlFields: 'id options', + }); + + const phaseFieldOptions = generateOptions(2); + + const { + data: { createOneField: createOnePhaseField }, + } = await createOneFieldMetadata({ + expectToFail: false, + input: { + objectMetadataId: createOneObject.id, + type: FieldMetadataType.SELECT, + name: 'phaseField', + label: 'Phase Field', + isLabelSyncedWithName: true, + options: phaseFieldOptions, + }, + gqlFields: 'id options', + }); + + const { + data: { createCoreView: view }, + } = await createOneCoreView({ + input: { + id: faker.string.uuid(), + icon: 'IconKanban', + name: 'Kanban View', + objectMetadataId: createOneObject.id, + type: ViewType.KANBAN, + mainGroupByFieldMetadataId: createOneStatusField.id, + }, + expectToFail: false, + gqlFields: 'id', + }); + + const { + data: { createCoreView: groupedTableView }, + } = await createOneCoreView({ + input: { + id: faker.string.uuid(), + icon: 'IconKanban', + name: 'Grouped Table View', + objectMetadataId: createOneObject.id, + type: ViewType.TABLE, + mainGroupByFieldMetadataId: createOneStatusField.id, + }, + expectToFail: false, + gqlFields: 'id', + }); + + return { + objectMetadataId: createOneObject.id, + statusFieldMetadataId: createOneStatusField.id, + phaseFieldMetadataId: createOnePhaseField.id, + statusFieldOptions: createOneStatusField.options ?? [], + viewId: view.id, + groupedTableViewId: groupedTableView.id, + phaseFieldOptions, + }; + }; + + afterEach(async () => { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: objectMetadataIdToDelete, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: objectMetadataIdToDelete }, + }); + }); + + describe.each(testFieldMetadataTypes)('%s field type', () => { + it('should delete all view groups when mainGroupByFieldMetadataId is removed', async () => { + const initialOptions = generateOptions(3); + + const { groupedTableViewId } = + await createObjectWithTwoSelectFieldsAndKanbanViewAndGroupedTableView( + initialOptions, + ); + + const { + data: { getCoreViewGroups: initialViewGroups }, + } = await findCoreViewGroups({ + viewId: groupedTableViewId, + gqlFields: 'id fieldValue', + expectToFail: false, + }); + + expect(initialViewGroups.length).toBe(4); + + await updateOneCoreView({ + viewId: groupedTableViewId, + input: { + id: groupedTableViewId, + mainGroupByFieldMetadataId: null, + }, + gqlFields: 'id mainGroupByFieldMetadataId', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId: groupedTableViewId, + gqlFields: 'id fieldValue', + expectToFail: false, + }); + + expect(updatedViewGroups.length).toBe(0); + }); + + it('should delete and recreate view groups when mainGroupByFieldMetadataId changes', async () => { + const initialOptions = generateOptions(3); + + const { phaseFieldOptions, phaseFieldMetadataId, viewId } = + await createObjectWithTwoSelectFieldsAndKanbanViewAndGroupedTableView( + initialOptions, + ); + + const { + data: { getCoreViewGroups: initialViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue', + expectToFail: false, + }); + + expect(initialViewGroups.length).toBe(initialOptions.length + 1); // null option + + await updateOneCoreView({ + viewId, + input: { + id: viewId, + mainGroupByFieldMetadataId: phaseFieldMetadataId, + }, + gqlFields: 'id mainGroupByFieldMetadataId', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue', + expectToFail: false, + }); + + expect(updatedViewGroups.length).toBe(phaseFieldOptions.length + 1); + + const actualFieldValues = updatedViewGroups + .map((viewGroup) => viewGroup.fieldValue) + .sort(); + const expectedFieldValues = [...phaseFieldOptions, { value: '' }] + .map((option) => option.value) + .sort(); + + expect(actualFieldValues).toEqual(expectedFieldValues); + }); + + it('should preserve view groups when mainGroupByFieldMetadataId does not change', async () => { + const initialOptions = generateOptions(3); + + const { statusFieldOptions, viewId } = + await createObjectWithTwoSelectFieldsAndKanbanViewAndGroupedTableView( + initialOptions, + ); + + const { + data: { getCoreViewGroups: initialViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue', + expectToFail: false, + }); + + expect(initialViewGroups.length).toBe(statusFieldOptions.length + 1); + + await updateOneCoreView({ + viewId, + input: { + id: viewId, + name: 'Renamed Kanban View', + }, + gqlFields: 'id name mainGroupByFieldMetadataId', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue', + expectToFail: false, + }); + + expect(updatedViewGroups.length).toBe(statusFieldOptions.length + 1); + const actualFieldValues = updatedViewGroups + .map((viewGroup) => viewGroup.fieldValue) + .sort(); + const expectedFieldValues = [...statusFieldOptions, { value: '' }] + .map((option) => option.value) + .sort(); + + expect(actualFieldValues).toEqual(expectedFieldValues); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap b/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap index 229e77f118..cb3f671775 100644 --- a/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/rest/suites/__snapshots__/view-group.integration-spec.ts.snap @@ -24,8 +24,8 @@ exports[`View Group REST API POST /metadata/viewGroups should fail to create vie "errors": [ { "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", + "message": "Field value is required", + "userFriendlyMessage": "Field value is required", }, ], "flatEntityMinimalInformation": { diff --git a/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts index 2bce0c89b8..78c43a8805 100644 --- a/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts @@ -14,12 +14,8 @@ import { } from 'test/integration/rest/utils/view-rest-api.util'; import { assertViewGroupStructure } from 'test/integration/utils/view-test.util'; import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; -import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; import { FieldMetadataType } from 'twenty-shared/types'; -import { type ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto'; -import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; - describe('View Group REST API', () => { let testObjectMetadataId: string; let testFieldMetadataId: string; @@ -47,9 +43,14 @@ describe('View Group REST API', () => { const createFieldInput = { name: 'testField', label: 'Test Field', - type: FieldMetadataType.TEXT, + type: FieldMetadataType.SELECT, objectMetadataId: testObjectMetadataId, isLabelSyncedWithName: true, + options: [ + { label: 'Option 1', value: 'OPTION_1', color: 'blue', position: 0 }, + { label: 'Option 2', value: 'OPTION_2', color: 'red', position: 1 }, + { label: 'Option 3', value: 'OPTION_3', color: 'green', position: 2 }, + ], }; const { @@ -72,6 +73,7 @@ describe('View Group REST API', () => { const testView = await createTestViewWithRestApi({ name: 'Test View for Group Integration', objectMetadataId: testObjectMetadataId, + mainGroupByFieldMetadataId: testFieldMetadataId, }); testViewId = testView.id; @@ -106,17 +108,6 @@ describe('View Group REST API', () => { }); describe('GET /metadata/viewGroups', () => { - it('should return empty array when no view groups exist', async () => { - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/viewGroups?viewId=${testViewId}`, - bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(response.body).toEqual([]); - }); - it('should return all view groups for workspace when no viewId provided', async () => { const response = await makeRestAPIRequest({ method: 'get', @@ -129,16 +120,6 @@ describe('View Group REST API', () => { }); it('should return view groups for a specific view after creating one', async () => { - const viewGroup = await createTestViewGroupWithRestApi({ - viewId: testViewId, - fieldMetadataId: testFieldMetadataId, - fieldValue: 'test-field-value', - isVisible: true, - position: 0, - }); - - testViewGroupId = viewGroup.id; - const response = await makeRestAPIRequest({ method: 'get', path: `/metadata/viewGroups?viewId=${testViewId}`, @@ -148,79 +129,36 @@ describe('View Group REST API', () => { assertRestApiSuccessfulResponse(response); expect(Array.isArray(response.body)).toBe(true); - const returnedViewGroup = response.body.find( - (el: ViewGroupDTO) => el.id === viewGroup.id, - ); + const returnedViewGroups = response.body; - jestExpectToBeDefined(returnedViewGroup); + expect(returnedViewGroups).toHaveLength(4); + // For a nullable field with three options, we expect groups for OPTION_1, OPTION_2, OPTION_3, and '' (empty string) + const expectedFieldValues = ['OPTION_1', 'OPTION_2', 'OPTION_3', '']; - assertViewGroupStructure(returnedViewGroup, { - id: viewGroup.id, - viewId: testViewId, - fieldValue: 'test-field-value', - isVisible: true, - position: 0, + // Check structure and visibility for each group + expectedFieldValues.forEach((expectedFieldValue) => { + const group = returnedViewGroups.find( + (group: any) => group.fieldValue === expectedFieldValue, + ); + + expect(group).toBeDefined(); + expect(group.isVisible).toBe(true); + expect(group.viewId).toBe(testViewId); }); - - testViewGroupId = viewGroup.id; - }); - - it('should return multiple view groups for a view', async () => { - const viewGroup1 = await createTestViewGroupWithRestApi({ - viewId: testViewId, - fieldMetadataId: testFieldMetadataId, - fieldValue: 'group-1', - position: 0, - }); - - const viewGroup2 = await createTestViewGroupWithRestApi({ - viewId: testViewId, - fieldMetadataId: testFieldMetadataId, - fieldValue: 'group-2', - position: 1, - }); - - const response = await makeRestAPIRequest({ - method: 'get', - path: `/metadata/viewGroups?viewId=${testViewId}`, - bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, - }); - - assertRestApiSuccessfulResponse(response); - expect(Array.isArray(response.body)).toBe(true); - expect(response.body).toHaveLength(2); - - const group1 = response.body.find( - (group: ViewGroupEntity) => group.id === viewGroup1.id, - ); - const group2 = response.body.find( - (group: ViewGroupEntity) => group.id === viewGroup2.id, - ); - - assertViewGroupStructure(group1, { - fieldValue: 'group-1', - position: 0, - }); - - assertViewGroupStructure(group2, { - fieldValue: 'group-2', - position: 1, - }); - - testViewGroupId = viewGroup2.id; }); }); describe('GET /metadata/viewGroups/:id', () => { it('should return a specific view group by id', async () => { - const viewGroup = await createTestViewGroupWithRestApi({ - viewId: testViewId, - fieldMetadataId: testFieldMetadataId, - fieldValue: 'specific-group', - isVisible: false, + const viewGroupsFromViewReponse = await makeRestAPIRequest({ + method: 'get', + path: `/metadata/viewGroups?viewId=${testViewId}`, + bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - testViewGroupId = viewGroup.id; + const viewGroup = viewGroupsFromViewReponse.body.find( + (group: any) => group.fieldValue === 'OPTION_1', + ); const response = await makeRestAPIRequest({ method: 'get', @@ -232,8 +170,8 @@ describe('View Group REST API', () => { assertViewGroupStructure(response.body, { id: viewGroup.id, viewId: testViewId, - fieldValue: 'specific-group', - isVisible: false, + fieldValue: 'OPTION_1', + isVisible: true, }); testViewGroupId = viewGroup.id; @@ -244,7 +182,6 @@ describe('View Group REST API', () => { it('should create a new view group', async () => { const viewGroupData = { viewId: testViewId, - fieldMetadataId: testFieldMetadataId, fieldValue: 'new-group-value', isVisible: true, position: 5, diff --git a/packages/twenty-server/test/integration/rest/suites/view.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view.integration-spec.ts index bd712344d6..68486edb98 100644 --- a/packages/twenty-server/test/integration/rest/suites/view.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view.integration-spec.ts @@ -1,3 +1,4 @@ +import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; @@ -17,6 +18,7 @@ import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum' describe('View REST API', () => { let testObjectMetadataId: string; + let testSelectFieldMetadataId: string; let testViewId: string | undefined; beforeAll(async () => { @@ -35,6 +37,15 @@ describe('View REST API', () => { }); testObjectMetadataId = objectMetadataId; + + const { selectFieldMetadataId } = + await createOneSelectFieldMetadataForIntegrationTests({ + input: { + objectMetadataId, + }, + }); + + testSelectFieldMetadataId = selectFieldMetadataId; }); afterAll(async () => { @@ -129,6 +140,7 @@ describe('View REST API', () => { isCompact: true, openRecordIn: ViewOpenRecordIn.SIDE_PANEL, objectMetadataId: testObjectMetadataId, + mainGroupByFieldMetadataId: testSelectFieldMetadataId, }); testViewId = kanbanView.id; @@ -206,6 +218,7 @@ describe('View REST API', () => { type: ViewType.KANBAN, isCompact: true, openRecordIn: ViewOpenRecordIn.SIDE_PANEL, + mainGroupByFieldMetadataId: testSelectFieldMetadataId, }; const response = await makeRestAPIRequest({ diff --git a/packages/twenty-server/test/integration/utils/view-test.util.ts b/packages/twenty-server/test/integration/utils/view-test.util.ts index 8dc0702ddb..e6fa0caff9 100644 --- a/packages/twenty-server/test/integration/utils/view-test.util.ts +++ b/packages/twenty-server/test/integration/utils/view-test.util.ts @@ -78,7 +78,6 @@ export const assertViewGroupStructure = ( ) => { expect(viewGroup).toBeDefined(); expect(viewGroup.id).toBeDefined(); - expect(viewGroup.fieldMetadataId).toBeDefined(); expect(viewGroup.viewId).toBeDefined(); expect(viewGroup.fieldValue).toBeDefined(); expect(typeof viewGroup.isVisible).toBe('boolean');