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');