diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 12ff213f8c..7deb61626c 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -237,6 +237,7 @@ export enum AllMetadataName { viewFilter = 'viewFilter', viewFilterGroup = 'viewFilterGroup', viewGroup = 'viewGroup', + viewSort = 'viewSort', webhook = 'webhook' } @@ -1372,6 +1373,11 @@ export type DeleteViewGroupInput = { id: Scalars['UUID']; }; +export type DeleteViewSortInput = { + /** The id of the view sort to delete. */ + id: Scalars['UUID']; +}; + export type DeletedWorkspaceMember = { __typename?: 'DeletedWorkspaceMember'; avatarUrl?: Maybe; @@ -1401,6 +1407,11 @@ export type DestroyViewGroupInput = { id: Scalars['UUID']; }; +export type DestroyViewSortInput = { + /** The id of the view sort to destroy. */ + id: Scalars['UUID']; +}; + export type DomainRecord = { __typename?: 'DomainRecord'; key: Scalars['String']; @@ -2744,7 +2755,7 @@ export type MutationDeleteCoreViewGroupArgs = { export type MutationDeleteCoreViewSortArgs = { - id: Scalars['String']; + input: DeleteViewSortInput; }; @@ -2870,7 +2881,7 @@ export type MutationDestroyCoreViewGroupArgs = { export type MutationDestroyCoreViewSortArgs = { - id: Scalars['String']; + input: DestroyViewSortInput; }; @@ -3156,7 +3167,6 @@ export type MutationUpdateCoreViewGroupArgs = { export type MutationUpdateCoreViewSortArgs = { - id: Scalars['String']; input: UpdateViewSortInput; }; @@ -5022,10 +5032,14 @@ export type UpdateViewInput = { }; export type UpdateViewSortInput = { + /** The id of the view sort to update */ + id: Scalars['UUID']; + /** The view sort to update */ + update: UpdateViewSortInputUpdates; +}; + +export type UpdateViewSortInputUpdates = { direction?: InputMaybe; - fieldMetadataId?: InputMaybe; - id?: InputMaybe; - viewId?: InputMaybe; }; export type UpdateWebhookInput = { @@ -6272,7 +6286,7 @@ export type UpsertFieldsWidgetMutationVariables = Exact<{ }>; -export type UpsertFieldsWidgetMutation = { __typename?: 'Mutation', upsertFieldsWidget: { __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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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 UpsertFieldsWidgetMutation = { __typename?: 'Mutation', upsertFieldsWidget: { __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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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, createdAt: string, deletedAt?: string | null, updatedAt: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> } }; export type FindAllRecordPageLayoutsQueryVariables = Exact<{ [key: string]: never; }>; @@ -6844,18 +6858,18 @@ 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, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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 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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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, createdAt: string, deletedAt?: string | null, updatedAt: 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, 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 }; +export type ViewSortFragmentFragment = { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string, createdAt: string, deletedAt?: string | null, updatedAt: string }; export type CreateCoreViewMutationVariables = Exact<{ input: CreateViewInput; }>; -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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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 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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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, createdAt: string, deletedAt?: string | null, updatedAt: 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; @@ -6897,7 +6911,7 @@ export type CreateCoreViewSortMutationVariables = Exact<{ }>; -export type CreateCoreViewSortMutation = { __typename?: 'Mutation', createCoreViewSort: { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string } }; +export type CreateCoreViewSortMutation = { __typename?: 'Mutation', createCoreViewSort: { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string, createdAt: string, deletedAt?: string | null, updatedAt: string } }; export type CreateManyCoreViewFieldGroupsMutationVariables = Exact<{ inputs: Array | CreateViewFieldGroupInput; @@ -6963,7 +6977,7 @@ export type DeleteCoreViewGroupMutationVariables = Exact<{ 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']; + input: DeleteViewSortInput; }>; @@ -7005,7 +7019,7 @@ export type DestroyCoreViewGroupMutationVariables = Exact<{ 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']; + input: DestroyViewSortInput; }>; @@ -7017,7 +7031,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, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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 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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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, createdAt: string, deletedAt?: string | null, updatedAt: 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; @@ -7056,26 +7070,25 @@ export type UpdateCoreViewGroupMutationVariables = Exact<{ 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']; input: UpdateViewSortInput; }>; -export type UpdateCoreViewSortMutation = { __typename?: 'Mutation', updateCoreViewSort: { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string } }; +export type UpdateCoreViewSortMutation = { __typename?: 'Mutation', updateCoreViewSort: { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string, createdAt: string, deletedAt?: string | null, updatedAt: string } }; export type FindAllCoreViewsQueryVariables = Exact<{ viewTypes?: InputMaybe | ViewType>; }>; -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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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 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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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, createdAt: string, deletedAt?: string | null, updatedAt: string }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: string, isVisible: boolean, fieldValue: string, position: number, viewId: string, createdAt: string, updatedAt: string, deletedAt?: string | null }> }> }; export type FindFieldsWidgetCoreViewsQueryVariables = Exact<{ viewTypes?: InputMaybe | ViewType>; }>; -export type FindFieldsWidgetCoreViewsQuery = { __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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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 FindFieldsWidgetCoreViewsQuery = { __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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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, createdAt: string, deletedAt?: string | null, updatedAt: 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']; @@ -7110,7 +7123,7 @@ export type FindManyCoreViewSortsQueryVariables = Exact<{ }>; -export type FindManyCoreViewSortsQuery = { __typename?: 'Query', getCoreViewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string }> }; +export type FindManyCoreViewSortsQuery = { __typename?: 'Query', getCoreViewSorts: Array<{ __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string, createdAt: string, deletedAt?: string | null, updatedAt: string }> }; export type FindManyCoreViewsQueryVariables = Exact<{ objectMetadataId?: InputMaybe; @@ -7118,14 +7131,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, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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 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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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, createdAt: string, deletedAt?: string | null, updatedAt: 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, mainGroupByFieldMetadataId?: string | null, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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 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, shouldHideEmptyGroups: boolean, 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 }>, viewFieldGroups: Array<{ __typename?: 'CoreViewFieldGroup', id: string, name: string, position: number, isVisible: boolean, viewId: string, createdAt: string, updatedAt: string, deletedAt?: 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, createdAt: string, deletedAt?: string | null, updatedAt: 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']; @@ -7160,7 +7173,7 @@ export type FindOneCoreViewSortQueryVariables = Exact<{ }>; -export type FindOneCoreViewSortQuery = { __typename?: 'Query', getCoreViewSort?: { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string } | null }; +export type FindOneCoreViewSortQuery = { __typename?: 'Query', getCoreViewSort?: { __typename?: 'CoreViewSort', id: string, fieldMetadataId: string, direction: ViewSortDirection, viewId: string, createdAt: string, deletedAt?: string | null, updatedAt: string } | null }; export type DeleteWorkspaceInvitationMutationVariables = Exact<{ appTokenId: Scalars['String']; @@ -8250,6 +8263,9 @@ export const ViewSortFragmentFragmentDoc = gql` fieldMetadataId direction viewId + createdAt + deletedAt + updatedAt } `; export const ViewGroupFragmentFragmentDoc = gql` @@ -15601,8 +15617,8 @@ export type DeleteCoreViewGroupMutationHookResult = ReturnType; export type DeleteCoreViewGroupMutationOptions = Apollo.BaseMutationOptions; export const DeleteCoreViewSortDocument = gql` - mutation DeleteCoreViewSort($id: String!) { - deleteCoreViewSort(id: $id) + mutation DeleteCoreViewSort($input: DeleteViewSortInput!) { + deleteCoreViewSort(input: $input) } `; export type DeleteCoreViewSortMutationFn = Apollo.MutationFunction; @@ -15620,7 +15636,7 @@ export type DeleteCoreViewSortMutationFn = Apollo.MutationFunction; export type DestroyCoreViewGroupMutationOptions = Apollo.BaseMutationOptions; export const DestroyCoreViewSortDocument = gql` - mutation DestroyCoreViewSort($id: String!) { - destroyCoreViewSort(id: $id) + mutation DestroyCoreViewSort($input: DestroyViewSortInput!) { + destroyCoreViewSort(input: $input) } `; export type DestroyCoreViewSortMutationFn = Apollo.MutationFunction; @@ -15812,7 +15828,7 @@ export type DestroyCoreViewSortMutationFn = Apollo.MutationFunction; export type UpdateCoreViewGroupMutationOptions = Apollo.BaseMutationOptions; export const UpdateCoreViewSortDocument = gql` - mutation UpdateCoreViewSort($id: String!, $input: UpdateViewSortInput!) { - updateCoreViewSort(id: $id, input: $input) { + mutation UpdateCoreViewSort($input: UpdateViewSortInput!) { + updateCoreViewSort(input: $input) { ...ViewSortFragment } } @@ -16045,7 +16061,6 @@ export type UpdateCoreViewSortMutationFn = Apollo.MutationFunction { frontComponent: t`front component`, navigationMenuItem: t`navigation menu item`, webhook: t`webhook`, + viewSort: t`view sort`, } as const satisfies Record; const handleMetadataError = ( diff --git a/packages/twenty-front/src/modules/views/graphql/fragments/viewSortFragment.ts b/packages/twenty-front/src/modules/views/graphql/fragments/viewSortFragment.ts index 12712b5ee8..414bdca58f 100644 --- a/packages/twenty-front/src/modules/views/graphql/fragments/viewSortFragment.ts +++ b/packages/twenty-front/src/modules/views/graphql/fragments/viewSortFragment.ts @@ -6,5 +6,8 @@ export const VIEW_SORT_FRAGMENT = gql` fieldMetadataId direction viewId + createdAt + deletedAt + updatedAt } `; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewSort.ts b/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewSort.ts index 1bf62e3d50..182c0f62ef 100644 --- a/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewSort.ts +++ b/packages/twenty-front/src/modules/views/graphql/mutations/deleteCoreViewSort.ts @@ -1,7 +1,7 @@ import { gql } from '@apollo/client'; export const DELETE_CORE_VIEW_SORT = gql` - mutation DeleteCoreViewSort($id: String!) { - deleteCoreViewSort(id: $id) + mutation DeleteCoreViewSort($input: DeleteViewSortInput!) { + deleteCoreViewSort(input: $input) } `; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/destroyCoreViewSort.ts b/packages/twenty-front/src/modules/views/graphql/mutations/destroyCoreViewSort.ts index 0a7d906847..5be92cd2f3 100644 --- a/packages/twenty-front/src/modules/views/graphql/mutations/destroyCoreViewSort.ts +++ b/packages/twenty-front/src/modules/views/graphql/mutations/destroyCoreViewSort.ts @@ -1,7 +1,7 @@ import { gql } from '@apollo/client'; export const DESTROY_CORE_VIEW_SORT = gql` - mutation DestroyCoreViewSort($id: String!) { - destroyCoreViewSort(id: $id) + mutation DestroyCoreViewSort($input: DestroyViewSortInput!) { + destroyCoreViewSort(input: $input) } `; diff --git a/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewSort.ts b/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewSort.ts index 067852b1e5..cb4d481e63 100644 --- a/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewSort.ts +++ b/packages/twenty-front/src/modules/views/graphql/mutations/updateCoreViewSort.ts @@ -3,8 +3,8 @@ import { gql } from '@apollo/client'; export const UPDATE_CORE_VIEW_SORT = gql` ${VIEW_SORT_FRAGMENT} - mutation UpdateCoreViewSort($id: String!, $input: UpdateViewSortInput!) { - updateCoreViewSort(id: $id, input: $input) { + mutation UpdateCoreViewSort($input: UpdateViewSortInput!) { + updateCoreViewSort(input: $input) { ...ViewSortFragment } } diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewSortsToCurrentRecordSorts.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewSortsToCurrentRecordSorts.test.tsx index cba89a9cb4..a8b5adf62a 100644 --- a/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewSortsToCurrentRecordSorts.test.tsx +++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useApplyCurrentViewSortsToCurrentRecordSorts.test.tsx @@ -16,6 +16,7 @@ import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jes import { mockedCoreViews } from '~/testing/mock-data/generated/metadata/views/mock-views-data'; import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; import { useApplyCurrentViewSortsToCurrentRecordSorts } from '@/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts'; +import { type ViewSort } from '@/views/types/ViewSort'; const mockObjectMetadataItemNameSingular = 'company'; @@ -38,7 +39,8 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => { throw new Error('Missing mock field metadata item with type TEXT'); } - const mockViewSort: CoreViewSortEssential = { + const mockViewSort: ViewSort = { + __typename: 'ViewSort', id: 'sort-1', fieldMetadataId: mockFieldMetadataItem.id, direction: ViewSortDirection.ASC, @@ -103,6 +105,7 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => { expect(result.current.currentRecordSorts).toEqual([ { + __typename: 'ViewSort', id: mockViewSort.id, fieldMetadataId: mockViewSort.fieldMetadataId, direction: mockViewSort.direction, diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewSortAPIPersist.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewSortAPIPersist.ts index 257685f40e..4be9b31a84 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewSortAPIPersist.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewSortAPIPersist.ts @@ -1,107 +1,276 @@ import { useCallback } from 'react'; -import { CREATE_CORE_VIEW_SORT } from '@/views/graphql/mutations/createCoreViewSort'; -import { DESTROY_CORE_VIEW_SORT } from '@/views/graphql/mutations/destroyCoreViewSort'; -import { UPDATE_CORE_VIEW_SORT } from '@/views/graphql/mutations/updateCoreViewSort'; +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useTriggerViewSortOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewSortOptimisticEffect'; -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; -import { type GraphQLView } from '@/views/types/GraphQLView'; -import { useApolloClient } from '@apollo/client'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { CrudOperationType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { type CoreViewSort } from '~/generated-metadata/graphql'; +import { + type CreateCoreViewSortMutationVariables, + type DeleteCoreViewSortMutationVariables, + type DestroyCoreViewSortMutationVariables, + type UpdateCoreViewSortMutationVariables, + useCreateCoreViewSortMutation, + useDeleteCoreViewSortMutation, + useDestroyCoreViewSortMutation, + useUpdateCoreViewSortMutation, +} from '~/generated-metadata/graphql'; export const usePerformViewSortAPIPersist = () => { - const apolloClient = useApolloClient(); - const { triggerViewSortOptimisticEffect } = useTriggerViewSortOptimisticEffect(); + const [createCoreViewSortMutation] = useCreateCoreViewSortMutation(); + const [updateCoreViewSortMutation] = useUpdateCoreViewSortMutation(); + const [deleteCoreViewSortMutation] = useDeleteCoreViewSortMutation(); + const [destroyCoreViewSortMutation] = useDestroyCoreViewSortMutation(); + + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); const performViewSortAPICreate = useCallback( - ( - viewSortsToCreate: CoreViewSortEssential[], - view: Pick, - ) => { - if (!viewSortsToCreate.length) return; - return Promise.all( - viewSortsToCreate.map((viewSort) => - apolloClient.mutate({ - mutation: CREATE_CORE_VIEW_SORT, - variables: { - input: { - id: viewSort.id, - fieldMetadataId: viewSort.fieldMetadataId, - viewId: view.id, - direction: viewSort.direction, - } satisfies Partial, - }, - update: (_cache, { data }) => { - const createdViewSort = data?.createCoreViewSort; - if (!isDefined(createdViewSort)) return; + async ( + createCoreViewSortInputs: CreateCoreViewSortMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (createCoreViewSortInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } - triggerViewSortOptimisticEffect({ - createdViewSorts: [createdViewSort], - }); - }, - }), - ), - ); + try { + const results = await Promise.all( + createCoreViewSortInputs.map((variables) => + createCoreViewSortMutation({ + variables, + update: (_cache, { data }) => { + const createdViewSort = data?.createCoreViewSort; + if (!isDefined(createdViewSort)) { + return; + } + + triggerViewSortOptimisticEffect({ + createdViewSorts: [createdViewSort], + }); + }, + }), + ), + ); + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewSort', + operationType: CrudOperationType.CREATE, + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } }, - [apolloClient, triggerViewSortOptimisticEffect], + [ + triggerViewSortOptimisticEffect, + createCoreViewSortMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], ); const performViewSortAPIUpdate = useCallback( - (viewSortsToUpdate: CoreViewSortEssential[]) => { - if (!viewSortsToUpdate.length) return; - return Promise.all( - viewSortsToUpdate.map((viewSort) => - apolloClient.mutate({ - mutation: UPDATE_CORE_VIEW_SORT, - variables: { - id: viewSort.id, - input: { - direction: viewSort.direction, - } satisfies Partial, - }, - update: (_cache, { data }) => { - const updatedViewSort = data?.updateCoreViewSort; - if (!isDefined(updatedViewSort)) return; + async ( + updateCoreViewSortInputs: UpdateCoreViewSortMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (updateCoreViewSortInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } - triggerViewSortOptimisticEffect({ - updatedViewSorts: [updatedViewSort], - }); - }, - }), - ), - ); + try { + const results = await Promise.all( + updateCoreViewSortInputs.map((variables) => + updateCoreViewSortMutation({ + variables, + update: (_cache, { data }) => { + const updatedViewSort = data?.updateCoreViewSort; + if (!isDefined(updatedViewSort)) { + return; + } + + triggerViewSortOptimisticEffect({ + updatedViewSorts: [updatedViewSort], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewSort', + operationType: CrudOperationType.UPDATE, + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred` }); + } + + return { + status: 'failed', + error, + }; + } }, - [apolloClient, triggerViewSortOptimisticEffect], + [ + triggerViewSortOptimisticEffect, + updateCoreViewSortMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], ); const performViewSortAPIDelete = useCallback( - (viewSortsToDelete: Pick[]) => { - if (!viewSortsToDelete.length) return; - return Promise.all( - viewSortsToDelete.map((viewSort) => - apolloClient.mutate({ - mutation: DESTROY_CORE_VIEW_SORT, - variables: { - id: viewSort.id, - }, - update: (_cache) => { - triggerViewSortOptimisticEffect({ - deletedViewSorts: [viewSort], - }); - }, - }), - ), - ); + async ( + deleteCoreViewSortInputs: DeleteCoreViewSortMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (deleteCoreViewSortInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + deleteCoreViewSortInputs.map((variables) => + deleteCoreViewSortMutation({ + variables, + update: (_cache, { data }) => { + const deletedViewSort = data?.deleteCoreViewSort; + + if (!isDefined(deletedViewSort)) { + return; + } + + triggerViewSortOptimisticEffect({ + deletedViewSorts: [ + { + id: variables.input.id, + }, + ], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewSort', + operationType: CrudOperationType.DELETE, + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred` }); + } + + return { + status: 'failed', + error, + }; + } }, - [apolloClient, triggerViewSortOptimisticEffect], + [ + triggerViewSortOptimisticEffect, + deleteCoreViewSortMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const performViewSortAPIDestroy = useCallback( + async ( + destroyCoreViewSortInputs: DestroyCoreViewSortMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (destroyCoreViewSortInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + destroyCoreViewSortInputs.map((variables) => + destroyCoreViewSortMutation({ + variables, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewSort', + operationType: CrudOperationType.DESTROY, + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [destroyCoreViewSortMutation, handleMetadataError, enqueueErrorSnackBar], ); return { performViewSortAPICreate, performViewSortAPIUpdate, performViewSortAPIDelete, + performViewSortAPIDestroy, }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useApplyCoreViewsForObjectMetadataId.ts b/packages/twenty-front/src/modules/views/hooks/useApplyCoreViewsForObjectMetadataId.ts index deeaf3dc0a..eb7936d63e 100644 --- a/packages/twenty-front/src/modules/views/hooks/useApplyCoreViewsForObjectMetadataId.ts +++ b/packages/twenty-front/src/modules/views/hooks/useApplyCoreViewsForObjectMetadataId.ts @@ -106,7 +106,14 @@ export const useApplyCoreViewsForObjectMetadataId = () => { ); } - if (!isDeeplyEqual(coreView.viewSorts, existingView.viewSorts)) { + if ( + !isDeeplyEqual( + coreView.viewSorts.map((viewSort) => + removePropertiesFromRecord(viewSort, ['createdAt', 'updatedAt']), + ), + existingView.viewSorts, + ) + ) { const view = convertCoreViewToView(coreView); jotaiStore.set( currentRecordSortsComponentState.atomFamily({ diff --git a/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts b/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts index 2b019d97dc..9157dae32e 100644 --- a/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts +++ b/packages/twenty-front/src/modules/views/hooks/useAreViewSortsDifferentFromRecordSorts.ts @@ -20,7 +20,7 @@ export const useAreViewSortsDifferentFromRecordSorts = () => { return false; } const viewSortsFromCurrentRecordSorts = currentRecordSorts.map( - (recordSort) => mapRecordSortToViewSort(recordSort, currentView.id), + (recordSort) => mapRecordSortToViewSort(recordSort), ); const viewSortsToCreate = getViewSortsToCreate( diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index 5964910bf1..ea6e35e48a 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -197,7 +197,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { }); const viewSortsToCreate = currentRecordSorts - .map((recordSort) => mapRecordSortToViewSort(recordSort, newViewId)) + .map((recordSort) => mapRecordSortToViewSort(recordSort)) .map((viewSort) => ({ ...viewSort, id: v4(), @@ -230,7 +230,20 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { return undefined; } - await performViewSortAPICreate(viewSortsToCreate, { id: newViewId }); + const createViewSortInputs = viewSortsToCreate.map((viewSort) => ({ + input: { + id: viewSort.id, + fieldMetadataId: viewSort.fieldMetadataId, + viewId: newViewId, + direction: viewSort.direction, + }, + })); + + const sortResult = await performViewSortAPICreate(createViewSortInputs); + + if (sortResult.status === 'failed') { + return undefined; + } } await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts index 943b0fbb92..e2820a7155 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts @@ -36,9 +36,7 @@ export const useSaveRecordSortsToViewSorts = () => { const currentRecordSorts = store.get(currentRecordSortsCallbackState); - const newViewSorts = currentRecordSorts.map((recordSort) => - mapRecordSortToViewSort(recordSort, currentView.id), - ); + const newViewSorts = currentRecordSorts.map(mapRecordSortToViewSort); const viewSortsToCreate = getViewSortsToCreate( currentViewSorts, @@ -55,13 +53,48 @@ export const useSaveRecordSortsToViewSorts = () => { newViewSorts, ); - await performViewSortAPICreate(viewSortsToCreate, currentView); - await performViewSortAPIUpdate(viewSortsToUpdate); - await performViewSortAPIDelete(viewSortsToDelete); + const createViewSortInputs = viewSortsToCreate.map((viewSort) => ({ + input: { + id: viewSort.id, + fieldMetadataId: viewSort.fieldMetadataId, + viewId: currentView.id, + direction: viewSort.direction, + }, + })); + + const updateViewSortInputs = viewSortsToUpdate.map((viewSort) => ({ + input: { + id: viewSort.id, + update: { + direction: viewSort.direction, + }, + }, + })); + + const deleteViewSortInputs = viewSortsToDelete.map((viewSort) => ({ + input: { + id: viewSort.id, + }, + })); + + const createResult = await performViewSortAPICreate(createViewSortInputs); + if (createResult.status === 'failed') { + return; + } + + const updateResult = await performViewSortAPIUpdate(updateViewSortInputs); + if (updateResult.status === 'failed') { + return; + } + + const deleteResult = await performViewSortAPIDelete(deleteViewSortInputs); + if (deleteResult.status === 'failed') { + return; + } }, [ - store, canPersistChanges, currentView, + store, currentRecordSortsCallbackState, performViewSortAPICreate, performViewSortAPIUpdate, diff --git a/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewSortOptimisticEffect.ts b/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewSortOptimisticEffect.ts index 2662acd56c..8cbfd96373 100644 --- a/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewSortOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/views/optimistic-effects/hooks/useTriggerViewSortOptimisticEffect.ts @@ -1,11 +1,11 @@ import { coreViewsState } from '@/views/states/coreViewState'; import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations'; import { useApolloClient } from '@apollo/client'; +import { useStore } from 'jotai'; import { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { type CoreViewSort } from '~/generated-metadata/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; -import { useStore } from 'jotai'; export const useTriggerViewSortOptimisticEffect = () => { const store = useStore(); @@ -21,7 +21,7 @@ export const useTriggerViewSortOptimisticEffect = () => { }: { createdViewSorts?: Omit[]; updatedViewSorts?: Omit[]; - deletedViewSorts?: Pick[]; + deletedViewSorts?: Pick[]; }) => { const coreViews = store.get(coreViewsState.atom); let newCoreViews = [...coreViews]; @@ -93,40 +93,45 @@ export const useTriggerViewSortOptimisticEffect = () => { } }); - deletedViewSorts.forEach( - (deletedViewSort: Pick) => { - cache.modify({ - id: cache.identify({ - __typename: 'CoreView', - id: deletedViewSort.viewId, - }), - fields: { - viewSorts: (existingViewSorts, { readField }) => - existingViewSorts.filter( - (viewSort) => - readField('id', viewSort) !== deletedViewSort.id, - ), - }, - }); - const toBeModifiedCoreView = newCoreViews.find( - (coreView) => coreView.id === deletedViewSort.viewId, - ); + deletedViewSorts.forEach((deletedViewSort: Pick) => { + const viewId = coreViews.find((coreView) => + coreView.viewSorts.some( + (viewSort) => viewSort.id === deletedViewSort.id, + ), + )?.id; - if (isDefined(toBeModifiedCoreView)) { - newCoreViews = [ - ...newCoreViews.filter( - (coreView) => coreView.id !== deletedViewSort.viewId, + if (!viewId) { + return; + } + + cache.modify({ + id: cache.identify({ + __typename: 'CoreView', + id: viewId, + }), + fields: { + viewSorts: (existingViewSorts, { readField }) => + existingViewSorts.filter( + (viewSort) => readField('id', viewSort) !== deletedViewSort.id, ), - { - ...toBeModifiedCoreView, - viewSorts: toBeModifiedCoreView.viewSorts.filter( - (viewSort) => viewSort.id !== deletedViewSort.id, - ), - }, - ]; - } - }, - ); + }, + }); + const toBeModifiedCoreView = newCoreViews.find( + (coreView) => coreView.id === viewId, + ); + + if (isDefined(toBeModifiedCoreView)) { + newCoreViews = [ + ...newCoreViews.filter((coreView) => coreView.id !== viewId), + { + ...toBeModifiedCoreView, + viewSorts: toBeModifiedCoreView.viewSorts.filter( + (viewSort) => viewSort.id !== deletedViewSort.id, + ), + }, + ]; + } + }); if (!isDeeplyEqual(coreViews, newCoreViews)) { store.set(coreViewsState.atom, newCoreViews); diff --git a/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts b/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts index d7fd848048..71874c12d3 100644 --- a/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts +++ b/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts @@ -1,4 +1,3 @@ -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; import { type AggregateOperations, type CoreViewField, @@ -6,6 +5,7 @@ import { type CoreViewFilter, type CoreViewFilterGroup, type CoreViewGroup, + type CoreViewSort, type ViewCalendarLayout, type ViewKey, type ViewOpenRecordIn, @@ -43,7 +43,7 @@ export type CoreViewWithRelations = { CoreViewFilterGroup, 'workspaceId' | 'createdAt' | 'updatedAt' >[]; - viewSorts: CoreViewSortEssential[]; + viewSorts: Omit[]; mainGroupByFieldMetadataId?: string | null; shouldHideEmptyGroups: boolean; kanbanAggregateOperation?: AggregateOperations | null; diff --git a/packages/twenty-front/src/modules/views/types/GraphQLView.ts b/packages/twenty-front/src/modules/views/types/GraphQLView.ts index 7212731d89..be802f8ab9 100644 --- a/packages/twenty-front/src/modules/views/types/GraphQLView.ts +++ b/packages/twenty-front/src/modules/views/types/GraphQLView.ts @@ -1,5 +1,4 @@ import { type AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; import { type ViewField } from '@/views/types/ViewField'; import { type ViewFilter } from '@/views/types/ViewFilter'; import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup'; @@ -11,6 +10,7 @@ import { type ViewVisibility, type ViewCalendarLayout, } from '~/generated-metadata/graphql'; +import { type ViewSort } from '@/views/types/ViewSort'; export type GraphQLView = { id: string; name: string; @@ -26,7 +26,7 @@ export type GraphQLView = { viewFields: ViewField[]; viewFilters: ViewFilter[]; viewFilterGroups?: ViewFilterGroup[]; - viewSorts: CoreViewSortEssential[]; + viewSorts: ViewSort[]; viewGroups: ViewGroup[]; position: number; icon: string; diff --git a/packages/twenty-front/src/modules/views/types/View.ts b/packages/twenty-front/src/modules/views/types/View.ts index 456f5d3c23..c2b5000f06 100644 --- a/packages/twenty-front/src/modules/views/types/View.ts +++ b/packages/twenty-front/src/modules/views/types/View.ts @@ -1,5 +1,4 @@ import { type AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; import { type ViewField } from '@/views/types/ViewField'; import { type ViewFieldGroup } from '@/views/types/ViewFieldGroup'; import { type ViewFilter } from '@/views/types/ViewFilter'; @@ -12,6 +11,7 @@ import { type ViewCalendarLayout, type ViewVisibility, } from '~/generated-metadata/graphql'; +import { type ViewSort } from '@/views/types/ViewSort'; export type View = { id: string; @@ -25,7 +25,7 @@ export type View = { viewGroups: ViewGroup[]; viewFilters: ViewFilter[]; viewFilterGroups?: ViewFilterGroup[]; - viewSorts: CoreViewSortEssential[]; + viewSorts: ViewSort[]; kanbanAggregateOperation: AggregateOperations | null; kanbanAggregateOperationFieldMetadataId: string | null; mainGroupByFieldMetadataId?: string | null; diff --git a/packages/twenty-front/src/modules/views/types/ViewSort.ts b/packages/twenty-front/src/modules/views/types/ViewSort.ts new file mode 100644 index 0000000000..2c9245f9e8 --- /dev/null +++ b/packages/twenty-front/src/modules/views/types/ViewSort.ts @@ -0,0 +1,11 @@ +import { type ViewSortDirection } from '~/generated-metadata/graphql'; + +export type ViewSort = { + __typename: 'ViewSort'; + id: string; + fieldMetadataId: string; + createdAt?: string; + updatedAt?: string; + direction: ViewSortDirection; + viewId?: string; +}; diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToCreate.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToCreate.test.ts index 4199a8b9f9..6508559cc2 100644 --- a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToCreate.test.ts +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToCreate.test.ts @@ -1,9 +1,10 @@ -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; import { ViewSortDirection } from '~/generated-metadata/graphql'; import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate'; +import { type ViewSort } from '@/views/types/ViewSort'; describe('getViewSortsToCreate', () => { - const baseSort: CoreViewSortEssential = { + const baseSort: ViewSort = { + __typename: 'ViewSort', id: 'sort-1', fieldMetadataId: 'field-1', direction: ViewSortDirection.ASC, @@ -11,14 +12,14 @@ describe('getViewSortsToCreate', () => { }; it('should return all sorts when current sorts array is empty', () => { - const currentViewSorts: CoreViewSortEssential[] = []; - const newViewSorts: CoreViewSortEssential[] = [ + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = [ { ...baseSort }, { ...baseSort, id: 'sort-2', fieldMetadataId: 'field-2', - } satisfies CoreViewSortEssential, + } satisfies ViewSort, ]; const result = getViewSortsToCreate(currentViewSorts, newViewSorts); @@ -27,8 +28,8 @@ describe('getViewSortsToCreate', () => { }); it('should return empty array when new sorts array is empty', () => { - const currentViewSorts: CoreViewSortEssential[] = [baseSort]; - const newViewSorts: CoreViewSortEssential[] = []; + const currentViewSorts: ViewSort[] = [baseSort]; + const newViewSorts: ViewSort[] = []; const result = getViewSortsToCreate(currentViewSorts, newViewSorts); @@ -41,11 +42,11 @@ describe('getViewSortsToCreate', () => { ...baseSort, id: 'sort-2', fieldMetadataId: 'field-2', - } satisfies CoreViewSortEssential; + } satisfies ViewSort; - const currentViewSorts: CoreViewSortEssential[] = [existingSort]; + const currentViewSorts: ViewSort[] = [existingSort]; - const newViewSorts: CoreViewSortEssential[] = [ + const newViewSorts: ViewSort[] = [ existingSort, newSortWithDifferentFieldMetadataId, ]; @@ -56,8 +57,8 @@ describe('getViewSortsToCreate', () => { }); it('should handle empty arrays for both inputs', () => { - const currentViewSorts: CoreViewSortEssential[] = []; - const newViewSorts: CoreViewSortEssential[] = []; + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = []; const result = getViewSortsToCreate(currentViewSorts, newViewSorts); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToDelete.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToDelete.test.ts index 7d6c4315b1..c60635f07c 100644 --- a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToDelete.test.ts +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToDelete.test.ts @@ -1,9 +1,10 @@ -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; +import { type ViewSort } from '@/views/types/ViewSort'; import { ViewSortDirection } from '~/generated-metadata/graphql'; import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete'; describe('getViewSortsToDelete', () => { - const baseSort: CoreViewSortEssential = { + const baseSort: ViewSort = { + __typename: 'ViewSort', id: 'sort-1', fieldMetadataId: 'field-1', direction: ViewSortDirection.ASC, @@ -11,8 +12,8 @@ describe('getViewSortsToDelete', () => { }; it('should return empty array when current sorts array is empty', () => { - const currentViewSorts: CoreViewSortEssential[] = []; - const newViewSorts: CoreViewSortEssential[] = [baseSort]; + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = [baseSort]; const result = getViewSortsToDelete(currentViewSorts, newViewSorts); @@ -21,8 +22,8 @@ describe('getViewSortsToDelete', () => { it('should return all current sorts when new sorts array is empty', () => { const existingSort = { ...baseSort }; - const currentViewSorts: CoreViewSortEssential[] = [existingSort]; - const newViewSorts: CoreViewSortEssential[] = []; + const currentViewSorts: ViewSort[] = [existingSort]; + const newViewSorts: ViewSort[] = []; const result = getViewSortsToDelete(currentViewSorts, newViewSorts); @@ -37,11 +38,8 @@ describe('getViewSortsToDelete', () => { fieldMetadataId: 'field-2', }; - const currentViewSorts: CoreViewSortEssential[] = [ - sortToDelete, - sortToKeep, - ]; - const newViewSorts: CoreViewSortEssential[] = [sortToKeep]; + const currentViewSorts: ViewSort[] = [sortToDelete, sortToKeep]; + const newViewSorts: ViewSort[] = [sortToKeep]; const result = getViewSortsToDelete(currentViewSorts, newViewSorts); @@ -49,8 +47,8 @@ describe('getViewSortsToDelete', () => { }); it('should handle empty arrays for both inputs', () => { - const currentViewSorts: CoreViewSortEssential[] = []; - const newViewSorts: CoreViewSortEssential[] = []; + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = []; const result = getViewSortsToDelete(currentViewSorts, newViewSorts); @@ -60,14 +58,15 @@ describe('getViewSortsToDelete', () => { it('should not delete sorts that match in both fieldMetadataId and direction', () => { const existingSort = { ...baseSort }; const matchingSort = { + __typename: 'ViewSort', id: 'sort-2', fieldMetadataId: 'field-1', direction: ViewSortDirection.ASC, viewId: 'view-1', - }; + } as ViewSort; - const currentViewSorts: CoreViewSortEssential[] = [existingSort]; - const newViewSorts: CoreViewSortEssential[] = [matchingSort]; + const currentViewSorts: ViewSort[] = [existingSort]; + const newViewSorts: ViewSort[] = [matchingSort]; const result = getViewSortsToDelete(currentViewSorts, newViewSorts); diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToUpdate.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToUpdate.test.ts index c863a37bef..6449e4c338 100644 --- a/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToUpdate.test.ts +++ b/packages/twenty-front/src/modules/views/utils/__tests__/getViewSortsToUpdate.test.ts @@ -1,9 +1,10 @@ -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; +import { type ViewSort } from '@/views/types/ViewSort'; import { ViewSortDirection } from '~/generated-metadata/graphql'; import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate'; describe('getViewSortsToUpdate', () => { - const baseSort: CoreViewSortEssential = { + const baseSort: ViewSort = { + __typename: 'ViewSort', id: 'sort-1', fieldMetadataId: 'field-1', direction: ViewSortDirection.ASC, @@ -11,8 +12,8 @@ describe('getViewSortsToUpdate', () => { }; it('should return empty array when current sorts array is empty', () => { - const currentViewSorts: CoreViewSortEssential[] = []; - const newViewSorts: CoreViewSortEssential[] = [baseSort]; + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = [baseSort]; const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); @@ -20,8 +21,8 @@ describe('getViewSortsToUpdate', () => { }); it('should return empty array when new sorts array is empty', () => { - const currentViewSorts: CoreViewSortEssential[] = [baseSort]; - const newViewSorts: CoreViewSortEssential[] = []; + const currentViewSorts: ViewSort[] = [baseSort]; + const newViewSorts: ViewSort[] = []; const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); @@ -33,10 +34,10 @@ describe('getViewSortsToUpdate', () => { const updatedSort = { ...baseSort, direction: ViewSortDirection.DESC, - } satisfies CoreViewSortEssential; + } satisfies ViewSort; - const currentViewSorts: CoreViewSortEssential[] = [existingSort]; - const newViewSorts: CoreViewSortEssential[] = [updatedSort]; + const currentViewSorts: ViewSort[] = [existingSort]; + const newViewSorts: ViewSort[] = [updatedSort]; const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); @@ -47,8 +48,8 @@ describe('getViewSortsToUpdate', () => { const existingSort = { ...baseSort }; const sameSort = { ...baseSort }; - const currentViewSorts: CoreViewSortEssential[] = [existingSort]; - const newViewSorts: CoreViewSortEssential[] = [sameSort]; + const currentViewSorts: ViewSort[] = [existingSort]; + const newViewSorts: ViewSort[] = [sameSort]; const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); @@ -56,8 +57,8 @@ describe('getViewSortsToUpdate', () => { }); it('should handle empty arrays for both inputs', () => { - const currentViewSorts: CoreViewSortEssential[] = []; - const newViewSorts: CoreViewSortEssential[] = []; + const currentViewSorts: ViewSort[] = []; + const newViewSorts: ViewSort[] = []; const result = getViewSortsToUpdate(currentViewSorts, newViewSorts); diff --git a/packages/twenty-front/src/modules/views/utils/convertCoreViewSortToViewSort.ts b/packages/twenty-front/src/modules/views/utils/convertCoreViewSortToViewSort.ts new file mode 100644 index 0000000000..3f5cf01016 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/convertCoreViewSortToViewSort.ts @@ -0,0 +1,17 @@ +import type { CoreViewSort } from '~/generated-metadata/graphql'; +import type { ViewSort } from '@/views/types/ViewSort'; + +export const convertCoreViewSortToViewSort = ( + coreViewSort: Pick< + CoreViewSort, + 'id' | 'direction' | 'fieldMetadataId' | 'viewId' + >, +): ViewSort => { + return { + __typename: 'ViewSort', + id: coreViewSort.id, + direction: coreViewSort.direction, + fieldMetadataId: coreViewSort.fieldMetadataId, + viewId: coreViewSort.viewId, + }; +}; diff --git a/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts b/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts index 7b970c66d0..d73c37087d 100644 --- a/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts +++ b/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts @@ -9,6 +9,7 @@ import { convertCoreViewKeyToViewKey } from '@/views/utils/convertCoreViewKeyToV import { convertCoreViewOpenRecordInToViewOpenRecordIn } from '@/views/utils/convertCoreViewOpenRecordInToViewOpenRecordIn'; import { convertCoreViewTypeToViewType } from '@/views/utils/convertCoreViewTypeToViewType'; import { ViewVisibility } from '~/generated-metadata/graphql'; +import { convertCoreViewSortToViewSort } from '@/views/utils/convertCoreViewSortToViewSort'; export const convertCoreViewToView = ( coreView: CoreViewWithRelations, @@ -41,7 +42,7 @@ export const convertCoreViewToView = ( viewFilterGroups: coreView.viewFilterGroups?.map( convertCoreViewFilterGroupToViewFilterGroup, ), - viewSorts: coreView.viewSorts, + viewSorts: coreView.viewSorts?.map(convertCoreViewSortToViewSort), mainGroupByFieldMetadataId: coreView.mainGroupByFieldMetadataId ?? null, shouldHideEmptyGroups: coreView.shouldHideEmptyGroups, kanbanAggregateOperation: coreView.kanbanAggregateOperation ?? null, diff --git a/packages/twenty-front/src/modules/views/utils/getViewSortsToCreate.ts b/packages/twenty-front/src/modules/views/utils/getViewSortsToCreate.ts index e39c0f241d..6bbd9c6575 100644 --- a/packages/twenty-front/src/modules/views/utils/getViewSortsToCreate.ts +++ b/packages/twenty-front/src/modules/views/utils/getViewSortsToCreate.ts @@ -1,15 +1,17 @@ -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; import { isDefined } from 'twenty-shared/utils'; -import { type CoreViewSort } from '~/generated-metadata/graphql'; +import { type ViewSort } from '@/views/types/ViewSort'; +import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined'; export const getViewSortsToCreate = ( - currentViewSorts: Pick[], - newViewSorts: CoreViewSortEssential[], + currentViewSorts: ViewSort[], + newViewSorts: ViewSort[], ) => { return newViewSorts.filter((newViewSort) => { - const correspondingViewSort = currentViewSorts.find( - (currentViewSort) => - currentViewSort.fieldMetadataId === newViewSort.fieldMetadataId, + const correspondingViewSort = currentViewSorts.find((currentViewSort) => + compareStrictlyExceptForNullAndUndefined( + currentViewSort.id, + newViewSort.id, + ), ); const shouldCreateBecauseViewSortIsNew = !isDefined(correspondingViewSort); diff --git a/packages/twenty-front/src/modules/views/utils/getViewSortsToDelete.ts b/packages/twenty-front/src/modules/views/utils/getViewSortsToDelete.ts index b03922c325..90383deb35 100644 --- a/packages/twenty-front/src/modules/views/utils/getViewSortsToDelete.ts +++ b/packages/twenty-front/src/modules/views/utils/getViewSortsToDelete.ts @@ -1,14 +1,22 @@ -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; +import { type ViewSort } from '@/views/types/ViewSort'; +import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined'; + +const isSameSortTarget = (sortA: ViewSort, sortB: ViewSort): boolean => + sortA.fieldMetadataId === sortB.fieldMetadataId && + sortA.direction === sortB.direction; export const getViewSortsToDelete = ( - currentViewSorts: CoreViewSortEssential[], - newViewSorts: Pick[], + currentViewSorts: ViewSort[], + newViewSorts: ViewSort[], ) => { return currentViewSorts.filter( (currentViewSort) => !newViewSorts.some( (newViewSort) => - newViewSort.fieldMetadataId === currentViewSort.fieldMetadataId, + compareStrictlyExceptForNullAndUndefined( + currentViewSort.id, + newViewSort.id, + ) || isSameSortTarget(currentViewSort, newViewSort), ), ); }; diff --git a/packages/twenty-front/src/modules/views/utils/getViewSortsToUpdate.ts b/packages/twenty-front/src/modules/views/utils/getViewSortsToUpdate.ts index 0c9797f9a6..265ed64918 100644 --- a/packages/twenty-front/src/modules/views/utils/getViewSortsToUpdate.ts +++ b/packages/twenty-front/src/modules/views/utils/getViewSortsToUpdate.ts @@ -1,18 +1,18 @@ -import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential'; import { areViewSortsEqual } from '@/views/utils/areViewSortsEqual'; import { isDefined } from 'twenty-shared/utils'; +import { type ViewSort } from '@/views/types/ViewSort'; +import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined'; export const getViewSortsToUpdate = ( - currentViewSorts: Pick< - CoreViewSortEssential, - 'fieldMetadataId' | 'direction' - >[], - newViewSorts: CoreViewSortEssential[], + currentViewSorts: ViewSort[], + newViewSorts: ViewSort[], ) => { return newViewSorts.filter((newViewSort) => { - const correspondingViewSort = currentViewSorts.find( - (currentViewSort) => - currentViewSort.fieldMetadataId === newViewSort.fieldMetadataId, + const correspondingViewSort = currentViewSorts.find((currentViewSort) => + compareStrictlyExceptForNullAndUndefined( + currentViewSort.id, + newViewSort.id, + ), ); if (!isDefined(correspondingViewSort)) { diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordSortToViewSort.ts b/packages/twenty-front/src/modules/views/utils/mapRecordSortToViewSort.ts index 32d37db27e..c24c381fe5 100644 --- a/packages/twenty-front/src/modules/views/utils/mapRecordSortToViewSort.ts +++ b/packages/twenty-front/src/modules/views/utils/mapRecordSortToViewSort.ts @@ -1,12 +1,11 @@ import { type RecordSort } from '@/object-record/record-sort/types/RecordSort'; -import { type CoreViewSort } from '~/generated-metadata/graphql'; +import { type ViewSort } from '@/views/types/ViewSort'; -export const mapRecordSortToViewSort = ( - recordSort: RecordSort, - viewId: string, -): Pick => { +export const mapRecordSortToViewSort = (recordSort: RecordSort): ViewSort => { return { - ...recordSort, - viewId, + __typename: 'ViewSort', + id: recordSort.id, + fieldMetadataId: recordSort.fieldMetadataId, + direction: recordSort.direction, }; }; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts index a6b1f7d21d..89a7333001 100644 --- a/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts @@ -97,6 +97,7 @@ export const fromFieldManifestToUniversalFlatFieldMetadata = ({ kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], createdAt: now, updatedAt: now, }; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-view-manifest-to-universal-flat-view.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-view-manifest-to-universal-flat-view.util.ts index a7d35245a9..7364695d15 100644 --- a/packages/twenty-server/src/engine/core-modules/application/utils/from-view-manifest-to-universal-flat-view.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-view-manifest-to-universal-flat-view.util.ts @@ -42,6 +42,7 @@ export const fromViewManifestToUniversalFlatView = ({ viewFilterGroupUniversalIdentifiers: [], viewGroupUniversalIdentifiers: [], viewFieldGroupUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], createdAt: now, updatedAt: now, deletedAt: null, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index cfbc818ef3..2086e1bb94 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -31,6 +31,7 @@ import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entiti import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; // This entity is used as a reference test case for type utilities in: // Modifying relations or properties may require updating type test expectations for Typecheck to pass. @@ -206,4 +207,7 @@ export class FieldMetadataEntity< @OneToMany(() => ViewEntity, (view) => view.mainGroupByFieldMetadata) mainGroupByFieldMetadataViews: Relation; + + @OneToMany(() => ViewSortEntity, (viewSort) => viewSort.fieldMetadata) + viewSorts: Relation; } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap index 3f787164d5..7876966aa8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should match snapshot 1`] = ` { @@ -299,6 +299,15 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma ], "propertiesToStringify": [], }, + "viewSort": { + "propertiesToCompare": [ + "direction", + "fieldMetadataUniversalIdentifier", + "viewUniversalIdentifier", + "deletedAt", + ], + "propertiesToStringify": [], + }, "webhook": { "propertiesToCompare": [ "targetUrl", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index 95ca7bb8d2..1192a49a3c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -1162,6 +1162,38 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { universalProperty: undefined, }, }, + viewSort: { + direction: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + fieldMetadataId: { + toCompare: true, + toStringify: false, + universalProperty: 'fieldMetadataUniversalIdentifier', + }, + createdAt: { + toCompare: false, + toStringify: false, + universalProperty: undefined, + }, + updatedAt: { + toCompare: false, + toStringify: false, + universalProperty: undefined, + }, + viewId: { + toCompare: true, + toStringify: false, + universalProperty: 'viewUniversalIdentifier', + }, + deletedAt: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, + }, viewFilterGroup: { viewId: { toCompare: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts index f6452789c2..ba806b76e0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts @@ -240,6 +240,16 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = { workspace: null, application: null, }, + viewSort: { + application: null, + workspace: null, + fieldMetadata: { + foreignKey: 'fieldMetadataId', + }, + view: { + foreignKey: 'viewId', + }, + }, } as const satisfies ManyToOneMetadataRelationsProperties; // satisfies with complex mapped types involving nested generics doesn't always catch missing required keys diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts index aa65e74529..157be325ae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts @@ -420,6 +420,24 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = { workspace: null, application: null, }, + viewSort: { + application: null, + workspace: null, + fieldMetadata: { + foreignKey: 'fieldMetadataId', + universalForeignKey: 'fieldMetadataUniversalIdentifier', + inverseOneToManyProperty: null, + isNullable: false, + metadataName: 'fieldMetadata', + }, + view: { + foreignKey: 'viewId', + universalForeignKey: 'viewUniversalIdentifier', + inverseOneToManyProperty: 'viewSorts', + isNullable: false, + metadataName: 'view', + }, + }, } as const satisfies ManyToOneMetadataRelationsProperties; // satisfies with complex mapped types involving nested generics doesn't always catch missing required keys diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts index 026d2192fb..66293ac247 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts @@ -24,6 +24,7 @@ import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-g import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; export const ALL_METADATA_ENTITY_BY_METADATA_NAME = { viewField: ViewFieldEntity, @@ -49,4 +50,5 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = { commandMenuItem: CommandMenuItemEntity, navigationMenuItem: NavigationMenuItemEntity, webhook: WebhookEntity, + viewSort: ViewSortEntity, } as const satisfies Record>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts index d8bacf032c..8a1a9dc4a5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts @@ -53,6 +53,10 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { viewFilterGroup: { view: true, }, + viewSort: { + fieldMetadata: true, + view: true, + }, role: {}, roleTarget: { role: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts index 99dfc98644..c719c06cb1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant.ts @@ -44,6 +44,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = { rowLevelPermissionPredicate: {}, rowLevelPermissionPredicateGroup: {}, viewFilterGroup: {}, + viewSort: {}, frontComponent: {}, webhook: {}, } as const satisfies MetadataSerializedRelationProperties; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts index 13cf63ef70..deaa608710 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts @@ -37,6 +37,11 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = { commandMenuItem: {}, navigationMenuItem: {}, fieldMetadata: { + viewSorts: { + flatEntityForeignKeyAggregator: 'viewSortIds', + metadataName: 'viewSort', + universalFlatEntityForeignKeyAggregator: 'viewSortUniversalIdentifiers', + }, fieldPermissions: null, indexFieldMetadatas: null, viewFields: { @@ -202,6 +207,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = { }, frontComponent: {}, webhook: {}, + viewSort: {}, } as const satisfies OneToManyMetadataRelationsProperties; // satisfies with complex mapped types involving nested generics doesn't always catch missing required keys diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts index db20232c9c..fc2019123d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts @@ -35,6 +35,8 @@ import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entiti import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; +import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service'; @Module({ imports: [ @@ -46,6 +48,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache ViewFilterEntity, ViewFilterGroupEntity, ViewGroupEntity, + ViewSortEntity, IndexMetadataEntity, IndexFieldMetadataEntity, FieldMetadataEntity, @@ -71,6 +74,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache WorkspaceFlatIndexMapCacheService, WorkspaceFlatFieldMetadataMapCacheService, WorkspaceFlatViewGroupMapCacheService, + WorkspaceFlatViewSortMapCacheService, WorkspaceFlatPageLayoutMapCacheService, WorkspaceFlatPageLayoutTabMapCacheService, WorkspaceFlatPageLayoutWidgetMapCacheService, @@ -88,6 +92,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache WorkspaceFlatIndexMapCacheService, WorkspaceFlatFieldMetadataMapCacheService, WorkspaceFlatViewGroupMapCacheService, + WorkspaceFlatViewSortMapCacheService, WorkspaceFlatPageLayoutMapCacheService, WorkspaceFlatPageLayoutTabMapCacheService, WorkspaceFlatPageLayoutWidgetMapCacheService, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/extract-entity-one-to-many-entity-relation-properties.type-test.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/extract-entity-one-to-many-entity-relation-properties.type-test.ts index 5f65a7b75a..e39904fc33 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/extract-entity-one-to-many-entity-relation-properties.type-test.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/extract-entity-one-to-many-entity-relation-properties.type-test.ts @@ -17,7 +17,7 @@ type FieldMetadataOneToManySyncableRelations = type Assertions = [ // FieldMetadataEntity OneToMany relations (all targets): // - indexFieldMetadatas, fieldPermissions, viewFields, viewFilters, - // - kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews + // - kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews, viewSorts Expect< Equal< FieldMetadataOneToManyRelations, @@ -28,6 +28,7 @@ type Assertions = [ | 'kanbanAggregateOperationViews' | 'calendarViews' | 'mainGroupByFieldMetadataViews' + | 'viewSorts' > >, @@ -40,6 +41,7 @@ type Assertions = [ | 'kanbanAggregateOperationViews' | 'calendarViews' | 'mainGroupByFieldMetadataViews' + | 'viewSorts' > >, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/extract-entity-related-entity-properties.type-test.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/extract-entity-related-entity-properties.type-test.ts index 3f93c3e9ea..60e89aa87f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/extract-entity-related-entity-properties.type-test.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/extract-entity-related-entity-properties.type-test.ts @@ -10,7 +10,7 @@ type FieldMetadataRelatedProperties = type Assertions = [ // FieldMetadataEntity has both ManyToOne and OneToMany relations // ManyToOne: object, workspace, application, relationTargetFieldMetadata, relationTargetObjectMetadata - // OneToMany: indexFieldMetadatas, fieldPermissions, viewFields, viewFilters, kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews + // OneToMany: indexFieldMetadatas, fieldPermissions, viewFields, viewFilters, kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews, viewSorts Expect< Equal< FieldMetadataRelatedProperties, @@ -26,6 +26,7 @@ type Assertions = [ | 'kanbanAggregateOperationViews' | 'calendarViews' | 'mainGroupByFieldMetadataViews' + | 'viewSorts' > >, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts index 5ce8ceb08b..31da675ab0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts @@ -33,6 +33,8 @@ import { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-f import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.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 { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type'; +import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type'; 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 { type FlatWebhookMaps } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook-maps.type'; @@ -65,6 +67,7 @@ import { type UniversalFlatViewFilterGroup } from 'src/engine/workspace-manager/ import { type UniversalFlatViewFilter } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter.type'; import { type UniversalFlatViewGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-group.type'; import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type'; +import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type'; import { type UniversalFlatWebhook } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-webhook.type'; import { type FlatCreateAgentAction, @@ -250,6 +253,14 @@ import { type UniversalDeleteWebhookAction, type UniversalUpdateWebhookAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type'; +import { + type FlatCreateViewSortAction, + type FlatDeleteViewSortAction, + type FlatUpdateViewSortAction, + type UniversalCreateViewSortAction, + type UniversalDeleteViewSortAction, + type UniversalUpdateViewSortAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type'; export type AllFlatEntityTypesByMetadataName = { fieldMetadata: { @@ -351,6 +362,22 @@ export type AllFlatEntityTypesByMetadataName = { universalFlatEntity: UniversalFlatViewGroup; entity: MetadataEntity<'viewGroup'>; }; + viewSort: { + flatEntityMaps: FlatViewSortMaps; + universalActions: { + create: UniversalCreateViewSortAction; + update: UniversalUpdateViewSortAction; + delete: UniversalDeleteViewSortAction; + }; + flatActions: { + create: FlatCreateViewSortAction; + update: FlatUpdateViewSortAction; + delete: FlatDeleteViewSortAction; + }; + flatEntity: FlatViewSort; + universalFlatEntity: UniversalFlatViewSort; + entity: MetadataEntity<'viewSort'>; + }; rowLevelPermissionPredicate: { flatEntityMaps: FlatRowLevelPermissionPredicateMaps; universalActions: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap index fed3a70aaa..404d5a6bc9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`getMetadataRelatedMetadataNames should return related metadata names for agent 1`] = `[]`; @@ -13,6 +13,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo [ "objectMetadata", "fieldMetadata", + "viewSort", "viewField", "viewFilter", "view", @@ -149,4 +150,11 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo ] `; +exports[`getMetadataRelatedMetadataNames should return related metadata names for viewSort 1`] = ` +[ + "fieldMetadata", + "view", +] +`; + exports[`getMetadataRelatedMetadataNames should return related metadata names for webhook 1`] = `[]`; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap index 065ccb2465..39067070b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap @@ -8,6 +8,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with "viewFilter", "commandMenuItem", "pageLayoutWidget", + "viewSort", "index", "pageLayout", "roleTarget", 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 916d7c434f..61b6d6551d 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 @@ -53,6 +53,8 @@ export const getFlatFieldMetadataMock = ( kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortIds: [], + viewSortUniversalIdentifiers: [], universalSettings: null, ...overrides, }; 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 f0abf087e2..15aaa4dce7 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 @@ -73,6 +73,8 @@ export const getRelationTargetFlatFieldMetadataMock = ({ kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortIds: [], + viewSortUniversalIdentifiers: [], universalSettings: settings, }; }; 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 0ba088aba1..094fc9b52d 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 @@ -147,6 +147,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "viewFieldUniversalIdentifiers": [], "viewFilterUniversalIdentifiers": [], + "viewSortUniversalIdentifiers": [], }, { "applicationUniversalIdentifier": Any, @@ -182,6 +183,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "viewFieldUniversalIdentifiers": [], "viewFilterUniversalIdentifiers": [], + "viewSortUniversalIdentifiers": [], }, { "applicationUniversalIdentifier": Any, @@ -215,6 +217,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "viewFieldUniversalIdentifiers": [], "viewFilterUniversalIdentifiers": [], + "viewSortUniversalIdentifiers": [], }, { "applicationUniversalIdentifier": Any, @@ -250,6 +253,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "viewFieldUniversalIdentifiers": [], "viewFilterUniversalIdentifiers": [], + "viewSortUniversalIdentifiers": [], }, ], "indexMetadatas": [ 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 fb2b1d22d7..141f047bce 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 @@ -20,6 +20,7 @@ import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace- import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; @Injectable() @WorkspaceCache('flatFieldMetadataMaps') @@ -39,6 +40,8 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro private readonly viewFilterRepository: Repository, @InjectRepository(ViewGroupEntity) private readonly viewGroupRepository: Repository, + @InjectRepository(ViewSortEntity) + private readonly viewSortRepository: Repository, @InjectRepository(ViewEntity) private readonly viewRepository: Repository, ) { @@ -54,6 +57,7 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro applications, viewFields, viewFilters, + viewSorts, views, ] = await Promise.all([ this.fieldMetadataRepository.find({ @@ -80,6 +84,11 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro select: ['id', 'universalIdentifier', 'fieldMetadataId'], withDeleted: true, }), + this.viewSortRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'fieldMetadataId'], + withDeleted: true, + }), this.viewRepository.find({ where: { workspaceId }, select: [ @@ -99,6 +108,7 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro calendarViewsByFieldId, kanbanViewsByFieldId, mainGroupByFieldMetadataViewsByFieldId, + viewSortsByFieldId, ] = ( [ { @@ -121,6 +131,10 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro entities: views, foreignKey: 'mainGroupByFieldMetadataId', }, + { + entities: viewSorts, + foreignKey: 'fieldMetadataId', + }, ] as const ).map(regroupEntitiesByRelatedEntityId); @@ -147,6 +161,7 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro mainGroupByFieldMetadataViewsByFieldId.get( fieldMetadataEntity.id, ) || [], + viewSorts: viewSortsByFieldId.get(fieldMetadataEntity.id) || [], }, fieldMetadataIdToUniversalIdentifierMap, objectMetadataIdToUniversalIdentifierMap, 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 30b8a2bffe..9887282fe6 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 @@ -127,6 +127,11 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = ({ fieldMetadataEntity.mainGroupByFieldMetadataViews?.map( ({ universalIdentifier }) => universalIdentifier, ) ?? [], + viewSortIds: fieldMetadataEntity.viewSorts?.map(({ id }) => id) ?? [], + viewSortUniversalIdentifiers: + fieldMetadataEntity.viewSorts?.map( + ({ universalIdentifier }) => universalIdentifier, + ) ?? [], universalSettings: settingsWithUniversalIdentifiers, }; }; 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 8bd295cf98..778568f793 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 @@ -57,5 +57,6 @@ export const getDefaultFlatFieldMetadata = ({ calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], universalSettings: settings ?? null, + viewSortUniversalIdentifiers: [], } as const satisfies UniversalFlatFieldMetadata; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/constants/flat-view-sort-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/constants/flat-view-sort-editable-properties.constant.ts new file mode 100644 index 0000000000..051f618a57 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/constants/flat-view-sort-editable-properties.constant.ts @@ -0,0 +1,5 @@ +import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant'; + +export const FLAT_VIEW_SORT_EDITABLE_PROPERTIES = [ + 'direction', +] as const satisfies MetadataEntityPropertyName<'viewSort'>[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service.ts new file mode 100644 index 0000000000..fef98eb5c6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type'; +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { fromViewSortEntityToFlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/utils/from-view-sort-entity-to-flat-view-sort.util'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; + +@Injectable() +@WorkspaceCache('flatViewSortMaps') +export class WorkspaceFlatViewSortMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ViewSortEntity) + private readonly viewSortRepository: Repository, + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + ) { + super(); + } + + async computeForCache(workspaceId: string): Promise { + const [existingViewSorts, applications, views, fieldMetadatas] = + await Promise.all([ + this.viewSortRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.fieldMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const viewIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(views); + const fieldMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(fieldMetadatas); + + const flatViewSortMaps = createEmptyFlatEntityMaps(); + + for (const viewSort of existingViewSorts) { + const flatViewSort = fromViewSortEntityToFlatViewSort({ + entity: viewSort, + applicationIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + }); + + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatViewSort, + flatEntityMapsToMutate: flatViewSortMaps, + }); + } + + return flatViewSortMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type.ts new file mode 100644 index 0000000000..ad562d79dc --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type'; + +export type FlatViewSortMaps = FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type.ts new file mode 100644 index 0000000000..94968a20b8 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type'; +import { type ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; + +export type FlatViewSort = FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-create-view-sort-input-to-flat-view-sort-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-create-view-sort-input-to-flat-view-sort-to-create.util.ts new file mode 100644 index 0000000000..ff270ab71c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-create-view-sort-input-to-flat-view-sort-to-create.util.ts @@ -0,0 +1,58 @@ +import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +import { type CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input'; +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type'; +import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util'; + +export const fromCreateViewSortInputToFlatViewSortToCreate = ({ + createViewSortInput: rawCreateViewSortInput, + flatApplication, + flatFieldMetadataMaps, + flatViewMaps, +}: { + createViewSortInput: CreateViewSortInput; + flatApplication: FlatApplication; +} & Pick< + AllFlatEntityMaps, + 'flatViewMaps' | 'flatFieldMetadataMaps' +>): UniversalFlatViewSort & { + id: string; +} => { + const { viewId, fieldMetadataId, ...createViewSortInput } = + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + rawCreateViewSortInput, + ['fieldMetadataId', 'id', 'viewId'], + ); + + const createdAt = new Date().toISOString(); + const viewSortId = createViewSortInput.id ?? v4(); + + const { fieldMetadataUniversalIdentifier, viewUniversalIdentifier } = + resolveEntityRelationUniversalIdentifiers({ + metadataName: 'viewSort', + foreignKeyValues: { + fieldMetadataId, + viewId, + }, + flatEntityMaps: { + flatFieldMetadataMaps, + flatViewMaps, + }, + }); + + return { + id: viewSortId, + fieldMetadataUniversalIdentifier, + viewUniversalIdentifier, + createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: createViewSortInput.universalIdentifier ?? v4(), + direction: createViewSortInput.direction ?? ViewSortDirection.ASC, + applicationUniversalIdentifier: flatApplication.universalIdentifier, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-delete-view-sort-input-to-flat-view-sort-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-delete-view-sort-input-to-flat-view-sort-or-throw.util.ts new file mode 100644 index 0000000000..954baf4147 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-delete-view-sort-input-to-flat-view-sort-or-throw.util.ts @@ -0,0 +1,44 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, +} from 'twenty-shared/utils'; + +import { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type'; +import { type DeleteViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input'; +import { + ViewSortException, + ViewSortExceptionCode, +} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception'; +import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; + +export const fromDeleteViewSortInputToFlatViewSortOrThrow = ({ + deleteViewSortInput: rawDeleteViewSortInput, + flatViewSortMaps, +}: { + deleteViewSortInput: DeleteViewSortInput; + flatViewSortMaps: FlatViewSortMaps; +}): UniversalFlatViewSort => { + const { id: viewSortId } = extractAndSanitizeObjectStringFields( + rawDeleteViewSortInput, + ['id'], + ); + + const existingFlatViewSortToDelete = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: viewSortId, + flatEntityMaps: flatViewSortMaps, + }); + + if (!isDefined(existingFlatViewSortToDelete)) { + throw new ViewSortException( + t`View sort not found`, + ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, + ); + } + + return { + ...existingFlatViewSortToDelete, + deletedAt: new Date().toISOString(), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-destroy-view-sort-input-to-flat-view-sort-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-destroy-view-sort-input-to-flat-view-sort-or-throw.util.ts new file mode 100644 index 0000000000..21a6298aa9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-destroy-view-sort-input-to-flat-view-sort-or-throw.util.ts @@ -0,0 +1,41 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, +} from 'twenty-shared/utils'; + +import { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type'; +import { type DestroyViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input'; +import { + ViewSortException, + ViewSortExceptionCode, +} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type'; + +export const fromDestroyViewSortInputToFlatViewSortOrThrow = ({ + destroyViewSortInput, + flatViewSortMaps, +}: { + destroyViewSortInput: DestroyViewSortInput; + flatViewSortMaps: FlatViewSortMaps; +}): UniversalFlatViewSort => { + const { id: viewSortId } = extractAndSanitizeObjectStringFields( + destroyViewSortInput, + ['id'], + ); + + const existingFlatViewSortToDestroy = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: viewSortId, + flatEntityMaps: flatViewSortMaps, + }); + + if (!isDefined(existingFlatViewSortToDestroy)) { + throw new ViewSortException( + t`View sort not found`, + ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, + ); + } + + return existingFlatViewSortToDestroy; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-update-view-sort-input-to-flat-view-sort-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-update-view-sort-input-to-flat-view-sort-to-update-or-throw.util.ts new file mode 100644 index 0000000000..505014a60f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-update-view-sort-input-to-flat-view-sort-to-update-or-throw.util.ts @@ -0,0 +1,54 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, +} from 'twenty-shared/utils'; + +import { FLAT_VIEW_SORT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-sort/constants/flat-view-sort-editable-properties.constant'; +import { type UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input'; +import { + ViewSortException, + ViewSortExceptionCode, +} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception'; +import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; +import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type'; +import { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; + +export const fromUpdateViewSortInputToFlatViewSortToUpdateOrThrow = ({ + updateViewSortInput: rawUpdateViewSortInput, + flatViewSortMaps, +}: { + updateViewSortInput: UpdateViewSortInput; + flatViewSortMaps: FlatViewSortMaps; +}): UniversalFlatViewSort => { + const { id: viewSortToUpdateId } = + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + rawUpdateViewSortInput, + ['id'], + ); + + const existingFlatViewSortToUpdate = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: viewSortToUpdateId, + flatEntityMaps: flatViewSortMaps, + }); + + if (!isDefined(existingFlatViewSortToUpdate)) { + throw new ViewSortException( + t`View sort not found`, + ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, + ); + } + + const updatedEditableSortProperties = extractAndSanitizeObjectStringFields( + rawUpdateViewSortInput.update, + FLAT_VIEW_SORT_EDITABLE_PROPERTIES, + ); + + return mergeUpdateInExistingRecord({ + existing: existingFlatViewSortToUpdate, + properties: FLAT_VIEW_SORT_EDITABLE_PROPERTIES, + update: updatedEditableSortProperties, + }); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-view-sort-entity-to-flat-view-sort.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-view-sort-entity-to-flat-view-sort.util.ts new file mode 100644 index 0000000000..552e2e288d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/utils/from-view-sort-entity-to-flat-view-sort.util.ts @@ -0,0 +1,63 @@ +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; + +import type { FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; +import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type'; + +export const fromViewSortEntityToFlatViewSort = ({ + entity: viewSortEntity, + applicationIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'viewSort'>): FlatViewSort => { + const viewSortEntityWithoutRelations = removePropertiesFromRecord( + viewSortEntity, + getMetadataEntityRelationProperties('viewSort'), + ); + + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(viewSortEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${viewSortEntity.applicationId} not found for viewSort ${viewSortEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get( + viewSortEntity.viewId, + ); + + if (!isDefined(viewUniversalIdentifier)) { + throw new FlatEntityMapsException( + `View with id ${viewSortEntity.viewId} not found for viewSort ${viewSortEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const fieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get(viewSortEntity.fieldMetadataId); + + if (!isDefined(fieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Field metadata with id ${viewSortEntity.fieldMetadataId} not found for viewSort ${viewSortEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return { + ...viewSortEntityWithoutRelations, + createdAt: viewSortEntity.createdAt.toISOString(), + updatedAt: viewSortEntity.updatedAt.toISOString(), + deletedAt: viewSortEntity.deletedAt?.toISOString() ?? null, + universalIdentifier: viewSortEntityWithoutRelations.universalIdentifier, + applicationUniversalIdentifier, + viewUniversalIdentifier, + fieldMetadataUniversalIdentifier, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/flat-view.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/flat-view.module.ts index 690c132cc0..81ab3862c5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/flat-view.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/flat-view.module.ts @@ -11,6 +11,7 @@ import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-g import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit ViewFieldGroupEntity, ViewFilterEntity, ViewGroupEntity, + ViewSortEntity, ViewFilterGroupEntity, ]), ], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts index 1df0608b5e..6893263aad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts @@ -16,6 +16,7 @@ import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; @@ -42,6 +43,8 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider, @InjectRepository(ViewFilterGroupEntity) private readonly viewFilterGroupRepository: Repository, + @InjectRepository(ViewSortEntity) + private readonly viewSortRepository: Repository, @InjectRepository(ViewFieldGroupEntity) private readonly viewFieldGroupRepository: Repository, ) { @@ -58,6 +61,7 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider ->; +export type FlatView = FlatEntityFrom; 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 0447d9579c..842bd6f7c8 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 @@ -91,6 +91,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({ viewGroupUniversalIdentifiers: [], viewFieldGroupUniversalIdentifiers: [], viewFilterGroupUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], applicationUniversalIdentifier: flatApplication.universalIdentifier, }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util.ts index 47413776b9..0cb717087d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util.ts @@ -121,5 +121,10 @@ export const fromViewEntityToFlatView = ({ viewEntity.viewFilterGroups?.map( ({ universalIdentifier }) => universalIdentifier, ) ?? [], + viewSortIds: viewEntity.viewSorts?.map(({ id }) => id) ?? [], + viewSortUniversalIdentifiers: + viewEntity.viewSorts?.map( + ({ universalIdentifier }) => universalIdentifier, + ) ?? [], }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant.ts index 5194e944bd..6f1dcf07e6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant.ts @@ -38,6 +38,7 @@ const PARTIAL_ID_FIELD = { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], } as const satisfies PartialSystemFlatFieldMetadata; const PARTIAL_CREATED_AT_FIELD = { @@ -65,6 +66,7 @@ const PARTIAL_CREATED_AT_FIELD = { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], } as const satisfies PartialSystemFlatFieldMetadata; const PARTIAL_UPDATED_AT_FIELD = { @@ -92,6 +94,7 @@ const PARTIAL_UPDATED_AT_FIELD = { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], } as const satisfies PartialSystemFlatFieldMetadata; const PARTIAL_DELETED_AT_FIELD = { @@ -119,6 +122,7 @@ const PARTIAL_DELETED_AT_FIELD = { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], } as const satisfies PartialSystemFlatFieldMetadata; const PARTIAL_CREATED_BY_FIELD = { @@ -146,6 +150,7 @@ const PARTIAL_CREATED_BY_FIELD = { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], } as const satisfies PartialSystemFlatFieldMetadata; const PARTIAL_UPDATED_BY_FIELD = { @@ -173,6 +178,7 @@ const PARTIAL_UPDATED_BY_FIELD = { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], } as const satisfies PartialSystemFlatFieldMetadata; const PARTIAL_POSITION_FIELD = { @@ -200,6 +206,7 @@ const PARTIAL_POSITION_FIELD = { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], } as const satisfies PartialSystemFlatFieldMetadata; const PARTIAL_SEARCH_VECTOR_FIELD = { @@ -229,6 +236,7 @@ const PARTIAL_SEARCH_VECTOR_FIELD = { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortUniversalIdentifiers: [], } as const satisfies PartialSystemFlatFieldMetadata; export const PARTIAL_SYSTEM_FLAT_FIELD_METADATAS = { 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 0f5d93f77c..5a1fee47a9 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 @@ -633,6 +633,7 @@ export class ObjectMetadataService extends TypeOrmQueryService['universalSettings'] = diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts index d91cc74488..37a7bb9eb4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/controllers/view-sort.controller.ts @@ -21,17 +21,17 @@ import { CreateViewSortPermissionGuard } from 'src/engine/metadata-modules/view- import { DeleteViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-sort-permission.guard'; import { UpdateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-sort-permission.guard'; import { CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input'; -import { UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input'; import { type ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto'; import { + generateViewSortExceptionMessage, + generateViewSortUserFriendlyExceptionMessage, ViewSortException, ViewSortExceptionCode, ViewSortExceptionMessageKey, - generateViewSortExceptionMessage, - generateViewSortUserFriendlyExceptionMessage, } from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception'; import { ViewSortRestApiExceptionFilter } from 'src/engine/metadata-modules/view-sort/filters/view-sort-rest-api-exception.filter'; import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service'; +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; @Controller('rest/metadata/viewSorts') @UseGuards(WorkspaceAuthGuard) @@ -82,11 +82,11 @@ export class ViewSortController { @UseGuards(CreateViewSortPermissionGuard) async create( @Body() input: CreateViewSortInput, - @AuthWorkspace() workspace: WorkspaceEntity, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return this.viewSortService.create({ - ...input, - workspaceId: workspace.id, + return await this.viewSortService.createOne({ + createViewSortInput: input, + workspaceId, }); } @@ -94,25 +94,25 @@ export class ViewSortController { @UseGuards(UpdateViewSortPermissionGuard) async update( @Param('id') id: string, - @Body() input: UpdateViewSortInput, - @AuthWorkspace() workspace: WorkspaceEntity, + @Body() input: { direction?: ViewSortDirection }, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - const updatedViewSort = await this.viewSortService.update( - id, - workspace.id, - input, - ); - - return updatedViewSort; + return await this.viewSortService.updateOne({ + updateViewSortInput: { id, update: input }, + workspaceId, + }); } @Delete(':id') @UseGuards(DeleteViewSortPermissionGuard) async delete( @Param('id') id: string, - @AuthWorkspace() workspace: WorkspaceEntity, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise<{ success: boolean }> { - const deletedViewSort = await this.viewSortService.delete(id, workspace.id); + const deletedViewSort = await this.viewSortService.deleteOne({ + deleteViewSortInput: { id }, + workspaceId, + }); return { success: isDefined(deletedViewSort) }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input.ts index 8a299cd3c7..f5cac72778 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input.ts @@ -1,22 +1,36 @@ -import { Field, InputType } from '@nestjs/graphql'; +import { Field, HideField, InputType } from '@nestjs/graphql'; + +import { IsEnum, IsOptional, IsUUID } from 'class-validator'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; @InputType() export class CreateViewSortInput { + @IsOptional() + @IsUUID() @Field(() => UUIDScalarType, { nullable: true }) id?: string; + @IsUUID() @Field(() => UUIDScalarType, { nullable: false }) fieldMetadataId: string; + @IsOptional() + @IsEnum(ViewSortDirection) @Field(() => ViewSortDirection, { nullable: true, defaultValue: ViewSortDirection.ASC, }) direction?: ViewSortDirection; + @IsUUID() @Field(() => UUIDScalarType, { nullable: false }) viewId: string; + + @HideField() + universalIdentifier?: string; + + @HideField() + applicationId?: string; } diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input.ts new file mode 100644 index 0000000000..2ca3681e90 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input.ts @@ -0,0 +1,15 @@ +import { InputType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class DeleteViewSortInput { + @IDField(() => UUIDScalarType, { + description: 'The id of the view sort to delete.', + }) + @IsUUID() + id: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input.ts new file mode 100644 index 0000000000..f370f4ff5d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input.ts @@ -0,0 +1,15 @@ +import { InputType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class DestroyViewSortInput { + @IDField(() => UUIDScalarType, { + description: 'The id of the view sort to destroy.', + }) + @IsUUID() + id: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input.ts index 8934c6f22f..76819be94c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input.ts @@ -1,6 +1,38 @@ -import { InputType, PartialType } from '@nestjs/graphql'; +import { Field, InputType } from '@nestjs/graphql'; -import { CreateViewSortInput } from './create-view-sort.input'; +import { + IsEnum, + IsNotEmpty, + IsOptional, + IsUUID, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; @InputType() -export class UpdateViewSortInput extends PartialType(CreateViewSortInput) {} +class UpdateViewSortInputUpdates { + @IsOptional() + @IsEnum(ViewSortDirection) + @Field(() => ViewSortDirection, { nullable: true }) + direction?: ViewSortDirection; +} + +@InputType() +export class UpdateViewSortInput { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType, { + description: 'The id of the view sort to update', + }) + id: string; + + @Type(() => UpdateViewSortInputUpdates) + @ValidateNested() + @Field(() => UpdateViewSortInputUpdates, { + description: 'The view sort to update', + }) + update: UpdateViewSortInputUpdates; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/resolvers/view-sort.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/resolvers/view-sort.resolver.ts index b9cd537998..1c3c3fddaa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/resolvers/view-sort.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/resolvers/view-sort.resolver.ts @@ -1,7 +1,7 @@ import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query } from '@nestjs/graphql'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined } from 'class-validator'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; @@ -17,6 +17,8 @@ import { UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/ import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto'; import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service'; import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter'; +import { DeleteViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input'; +import { DestroyViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input'; @MetadataResolver(() => ViewSortDTO) @UseFilters(ViewGraphqlApiExceptionFilter) @@ -42,40 +44,45 @@ export class ViewSortResolver { @UseGuards(NoPermissionGuard) async getCoreViewSort( @Args('id', { type: () => String }) id: string, - @AuthWorkspace() workspace: WorkspaceEntity, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return this.viewSortService.findById(id, workspace.id); + return this.viewSortService.findById(id, workspaceId); } @Mutation(() => ViewSortDTO) @UseGuards(CreateViewSortPermissionGuard) async createCoreViewSort( - @Args('input') input: CreateViewSortInput, - @AuthWorkspace() workspace: WorkspaceEntity, + @Args('input') createViewSortInput: CreateViewSortInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return this.viewSortService.create({ - ...input, - workspaceId: workspace.id, + return this.viewSortService.createOne({ + createViewSortInput, + workspaceId, }); } @Mutation(() => ViewSortDTO) @UseGuards(UpdateViewSortPermissionGuard) async updateCoreViewSort( - @Args('id', { type: () => String }) id: string, - @Args('input') input: UpdateViewSortInput, - @AuthWorkspace() workspace: WorkspaceEntity, + @Args('input') updateViewSortInput: UpdateViewSortInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return this.viewSortService.update(id, workspace.id, input); + return this.viewSortService.updateOne({ + updateViewSortInput, + workspaceId, + }); } @Mutation(() => Boolean) @UseGuards(DeleteViewSortPermissionGuard) async deleteCoreViewSort( - @Args('id', { type: () => String }) id: string, - @AuthWorkspace() workspace: WorkspaceEntity, + @Args('input') deleteViewSortInput: DeleteViewSortInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - const deletedViewSort = await this.viewSortService.delete(id, workspace.id); + const deletedViewSort = await this.viewSortService.deleteOne({ + deleteViewSortInput, + workspaceId, + }); return isDefined(deletedViewSort); } @@ -83,9 +90,14 @@ export class ViewSortResolver { @Mutation(() => Boolean) @UseGuards(DestroyViewSortPermissionGuard) async destroyCoreViewSort( - @Args('id', { type: () => String }) id: string, - @AuthWorkspace() workspace: WorkspaceEntity, + @Args('input') destroyViewSortInput: DestroyViewSortInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return this.viewSortService.destroy(id, workspace.id); + const destroyedViewSort = await this.viewSortService.destroyOne({ + destroyViewSortInput, + workspaceId, + }); + + return isDefined(destroyedViewSort); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/services/tests/view-sort.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/services/tests/view-sort.service.spec.ts deleted file mode 100644 index d4dd1bf65b..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/services/tests/view-sort.service.spec.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; - -import { type Repository } from 'typeorm'; - -import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; -import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; -import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; -import { - ViewSortException, - ViewSortExceptionCode, - ViewSortExceptionMessageKey, - generateViewSortExceptionMessage, - generateViewSortUserFriendlyExceptionMessage, -} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception'; -import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; - -describe('ViewSortService', () => { - let viewSortService: ViewSortService; - let viewSortRepository: Repository; - let applicationService: ApplicationService; - - const mockViewSort = { - id: 'view-sort-id', - fieldMetadataId: 'field-id', - viewId: 'view-id', - workspaceId: 'workspace-id', - direction: ViewSortDirection.ASC, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - } as ViewSortEntity; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ViewSortService, - { - provide: getRepositoryToken(ViewSortEntity), - useValue: { - find: jest.fn(), - findOne: jest.fn(), - create: jest.fn(), - save: jest.fn(), - softDelete: jest.fn(), - delete: jest.fn(), - }, - }, - { - provide: WorkspaceCacheStorageService, - useValue: { - flushGraphQLOperation: jest.fn(), - }, - }, - { - provide: ApplicationService, - useValue: { - findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest.fn(), - }, - }, - ], - }).compile(); - - viewSortService = module.get(ViewSortService); - viewSortRepository = module.get>( - getRepositoryToken(ViewSortEntity), - ); - applicationService = module.get(ApplicationService); - }); - - it('should be defined', () => { - expect(viewSortService).toBeDefined(); - }); - - describe('findByWorkspaceId', () => { - it('should return view sorts for a workspace', async () => { - const workspaceId = 'workspace-id'; - const expectedViewSorts = [mockViewSort]; - - jest - .spyOn(viewSortRepository, 'find') - .mockResolvedValue(expectedViewSorts); - - const result = await viewSortService.findByWorkspaceId(workspaceId); - - expect(viewSortRepository.find).toHaveBeenCalledWith({ - where: { - workspaceId, - deletedAt: expect.anything(), - }, - relations: ['workspace', 'view'], - }); - expect(result).toEqual(expectedViewSorts); - }); - }); - - describe('findByViewId', () => { - it('should return view sorts for a view', async () => { - const workspaceId = 'workspace-id'; - const viewId = 'view-id'; - const expectedViewSorts = [mockViewSort]; - - jest - .spyOn(viewSortRepository, 'find') - .mockResolvedValue(expectedViewSorts); - - const result = await viewSortService.findByViewId(workspaceId, viewId); - - expect(viewSortRepository.find).toHaveBeenCalledWith({ - where: { - workspaceId, - viewId, - deletedAt: expect.anything(), - }, - relations: ['workspace', 'view'], - }); - expect(result).toEqual(expectedViewSorts); - }); - }); - - describe('findById', () => { - it('should return a view sort by id', async () => { - const id = 'view-sort-id'; - const workspaceId = 'workspace-id'; - - jest.spyOn(viewSortRepository, 'findOne').mockResolvedValue(mockViewSort); - - const result = await viewSortService.findById(id, workspaceId); - - expect(viewSortRepository.findOne).toHaveBeenCalledWith({ - where: { - id, - workspaceId, - deletedAt: expect.anything(), - }, - relations: ['workspace', 'view'], - }); - expect(result).toEqual(mockViewSort); - }); - - it('should return null when view sort is not found', async () => { - const id = 'non-existent-id'; - const workspaceId = 'workspace-id'; - - jest.spyOn(viewSortRepository, 'findOne').mockResolvedValue(null); - - const result = await viewSortService.findById(id, workspaceId); - - expect(result).toBeNull(); - }); - }); - - describe('create', () => { - const validViewSortData = { - fieldMetadataId: 'field-id', - viewId: 'view-id', - workspaceId: 'workspace-id', - direction: ViewSortDirection.ASC, - }; - - it('should create a view sort successfully', async () => { - const mockApplicationId = 'application-id'; - - jest - .spyOn( - applicationService, - 'findWorkspaceTwentyStandardAndCustomApplicationOrThrow', - ) - .mockResolvedValue({ - workspaceCustomFlatApplication: { id: mockApplicationId }, - } as any); - jest.spyOn(viewSortRepository, 'create').mockReturnValue(mockViewSort); - jest.spyOn(viewSortRepository, 'save').mockResolvedValue(mockViewSort); - - const result = await viewSortService.create(validViewSortData); - - expect(viewSortRepository.create).toHaveBeenCalledWith({ - ...validViewSortData, - universalIdentifier: expect.any(String), - applicationId: mockApplicationId, - }); - expect(viewSortRepository.save).toHaveBeenCalledWith(mockViewSort); - expect(result).toEqual(mockViewSort); - }); - - it('should throw exception when workspaceId is missing', async () => { - const invalidData = { ...validViewSortData, workspaceId: undefined }; - - await expect(viewSortService.create(invalidData)).rejects.toThrow( - new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED, - ), - ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, - { - userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage( - ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED, - ), - }, - ), - ); - }); - - it('should throw exception when viewId is missing', async () => { - const invalidData = { ...validViewSortData, viewId: undefined }; - - jest - .spyOn( - applicationService, - 'findWorkspaceTwentyStandardAndCustomApplicationOrThrow', - ) - .mockResolvedValue({ - workspaceCustomFlatApplication: { id: 'application-id' }, - } as any); - - await expect(viewSortService.create(invalidData)).rejects.toThrow( - new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.VIEW_ID_REQUIRED, - ), - ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, - { - userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage( - ViewSortExceptionMessageKey.VIEW_ID_REQUIRED, - ), - }, - ), - ); - }); - - it('should throw exception when fieldMetadataId is missing', async () => { - const invalidData = { ...validViewSortData, fieldMetadataId: undefined }; - - jest - .spyOn( - applicationService, - 'findWorkspaceTwentyStandardAndCustomApplicationOrThrow', - ) - .mockResolvedValue({ - workspaceCustomFlatApplication: { id: 'application-id' }, - } as any); - - await expect(viewSortService.create(invalidData)).rejects.toThrow( - new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED, - ), - ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, - { - userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage( - ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED, - ), - }, - ), - ); - }); - }); - - describe('update', () => { - it('should update a view sort successfully', async () => { - const id = 'view-sort-id'; - const workspaceId = 'workspace-id'; - const updateData = { direction: ViewSortDirection.DESC }; - const updatedViewSort = { ...mockViewSort, ...updateData }; - - jest.spyOn(viewSortService, 'findById').mockResolvedValue(mockViewSort); - jest.spyOn(viewSortRepository, 'save').mockResolvedValue(updatedViewSort); - - const result = await viewSortService.update(id, workspaceId, updateData); - - expect(viewSortService.findById).toHaveBeenCalledWith(id, workspaceId); - expect(viewSortRepository.save).toHaveBeenCalledWith({ - id, - ...updateData, - }); - expect(result).toEqual({ ...mockViewSort, ...updatedViewSort }); - }); - - it('should throw exception when view sort is not found', async () => { - const id = 'non-existent-id'; - const workspaceId = 'workspace-id'; - const updateData = { direction: ViewSortDirection.DESC }; - - jest.spyOn(viewSortService, 'findById').mockResolvedValue(null); - - await expect( - viewSortService.update(id, workspaceId, updateData), - ).rejects.toThrow( - new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - id, - ), - ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, - ), - ); - }); - }); - - describe('delete', () => { - it('should delete a view sort successfully', async () => { - const id = 'view-sort-id'; - const workspaceId = 'workspace-id'; - - jest.spyOn(viewSortService, 'findById').mockResolvedValue(mockViewSort); - jest.spyOn(viewSortRepository, 'softDelete').mockResolvedValue({} as any); - - const result = await viewSortService.delete(id, workspaceId); - - expect(viewSortService.findById).toHaveBeenCalledWith(id, workspaceId); - expect(viewSortRepository.softDelete).toHaveBeenCalledWith(id); - expect(result).toEqual(mockViewSort); - }); - - it('should throw exception when view sort is not found', async () => { - const id = 'non-existent-id'; - const workspaceId = 'workspace-id'; - - jest.spyOn(viewSortService, 'findById').mockResolvedValue(null); - - await expect(viewSortService.delete(id, workspaceId)).rejects.toThrow( - new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - id, - ), - ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, - ), - ); - }); - }); - - describe('destroy', () => { - it('should destroy a view sort successfully', async () => { - const id = 'view-sort-id'; - const workspaceId = 'workspace-id'; - - jest.spyOn(viewSortRepository, 'findOne').mockResolvedValue(mockViewSort); - jest.spyOn(viewSortRepository, 'delete').mockResolvedValue({} as any); - - const result = await viewSortService.destroy(id, workspaceId); - - expect(viewSortRepository.findOne).toHaveBeenCalledWith({ - where: { id, workspaceId }, - relations: ['workspace', 'view'], - withDeleted: true, - }); - expect(viewSortRepository.delete).toHaveBeenCalledWith(id); - expect(result).toEqual(true); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/services/view-sort.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/services/view-sort.service.ts index acc7dd5c7a..1165830e31 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/services/view-sort.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/services/view-sort.service.ts @@ -1,32 +1,305 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { IsNull, Repository } from 'typeorm'; -import { v4 } from 'uuid'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { fromCreateViewSortInputToFlatViewSortToCreate } from 'src/engine/metadata-modules/flat-view-sort/utils/from-create-view-sort-input-to-flat-view-sort-to-create.util'; +import { fromDeleteViewSortInputToFlatViewSortOrThrow } from 'src/engine/metadata-modules/flat-view-sort/utils/from-delete-view-sort-input-to-flat-view-sort-or-throw.util'; +import { fromDestroyViewSortInputToFlatViewSortOrThrow } from 'src/engine/metadata-modules/flat-view-sort/utils/from-destroy-view-sort-input-to-flat-view-sort-or-throw.util'; +import { fromUpdateViewSortInputToFlatViewSortToUpdateOrThrow } from 'src/engine/metadata-modules/flat-view-sort/utils/from-update-view-sort-input-to-flat-view-sort-to-update-or-throw.util'; +import { CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input'; +import { DeleteViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input'; +import { DestroyViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input'; +import { UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input'; +import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto'; import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; -import { - ViewSortException, - ViewSortExceptionCode, - ViewSortExceptionMessageKey, - generateViewSortExceptionMessage, - generateViewSortUserFriendlyExceptionMessage, -} from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception'; -import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modules/view/constants/find-all-core-views-graphql-operation.constant'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { fromFlatViewSortToViewSortDto } from 'src/engine/metadata-modules/view-sort/utils/from-flat-view-sort-to-view-sort-dto.util'; +import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; -// TODO migrate to v2 @Injectable() export class ViewSortService { constructor( @InjectRepository(ViewSortEntity) private readonly viewSortRepository: Repository, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly applicationService: ApplicationService, ) {} + async createOne({ + createViewSortInput, + workspaceId, + }: { + createViewSortInput: CreateViewSortInput; + workspaceId: string; + }): Promise { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatFieldMetadataMaps, flatViewMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFieldMetadataMaps', 'flatViewMaps'], + }, + ); + + const flatViewSortToCreate = fromCreateViewSortInputToFlatViewSortToCreate({ + createViewSortInput, + flatApplication: workspaceCustomFlatApplication, + flatFieldMetadataMaps, + flatViewMaps, + }); + + const buildAndRunResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + viewSort: { + flatEntityToCreate: [flatViewSortToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (buildAndRunResult.status === 'fail') { + throw new WorkspaceMigrationBuilderException( + buildAndRunResult, + 'Multiple validation errors occurred while creating view sort', + ); + } + + const { flatViewSortMaps: recomputedExistingFlatViewSortMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewSortMaps'], + }, + ); + + return fromFlatViewSortToViewSortDto( + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatViewSortToCreate.id, + flatEntityMaps: recomputedExistingFlatViewSortMaps, + }), + ); + } + + async updateOne({ + updateViewSortInput, + workspaceId, + }: { + workspaceId: string; + updateViewSortInput: UpdateViewSortInput; + }): Promise { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatViewSortMaps: existingFlatViewSortMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewSortMaps'], + }, + ); + + const optimisticallyUpdatedFlatViewSort = + fromUpdateViewSortInputToFlatViewSortToUpdateOrThrow({ + flatViewSortMaps: existingFlatViewSortMaps, + updateViewSortInput, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + viewSort: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatViewSort], + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while updating view sort', + ); + } + + const { flatViewSortMaps: recomputedExistingFlatViewSortMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewSortMaps'], + }, + ); + + return fromFlatViewSortToViewSortDto( + findFlatEntityByUniversalIdentifierOrThrow({ + universalIdentifier: + optimisticallyUpdatedFlatViewSort.universalIdentifier, + flatEntityMaps: recomputedExistingFlatViewSortMaps, + }), + ); + } + + async deleteOne({ + deleteViewSortInput, + workspaceId, + }: { + deleteViewSortInput: DeleteViewSortInput; + workspaceId: string; + }): Promise { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatViewSortMaps: existingFlatViewSortMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewSortMaps'], + }, + ); + + const optimisticallyUpdatedFlatViewSortWithDeletedAt = + fromDeleteViewSortInputToFlatViewSortOrThrow({ + flatViewSortMaps: existingFlatViewSortMaps, + deleteViewSortInput, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + viewSort: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + optimisticallyUpdatedFlatViewSortWithDeletedAt, + ], + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting view sort', + ); + } + + const { flatViewSortMaps: recomputedExistingFlatViewSortMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewSortMaps'], + }, + ); + + return fromFlatViewSortToViewSortDto( + findFlatEntityByUniversalIdentifierOrThrow({ + universalIdentifier: + optimisticallyUpdatedFlatViewSortWithDeletedAt.universalIdentifier, + flatEntityMaps: recomputedExistingFlatViewSortMaps, + }), + ); + } + + async destroyOne({ + destroyViewSortInput, + workspaceId, + }: { + destroyViewSortInput: DestroyViewSortInput; + workspaceId: string; + }): Promise { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatViewSortMaps: existingFlatViewSortMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewSortMaps'], + }, + ); + + const existingViewSortToDelete = + fromDestroyViewSortInputToFlatViewSortOrThrow({ + destroyViewSortInput, + flatViewSortMaps: existingFlatViewSortMaps, + }); + + const existingFlatViewSort = findFlatEntityByUniversalIdentifierOrThrow({ + universalIdentifier: existingViewSortToDelete.universalIdentifier, + flatEntityMaps: existingFlatViewSortMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + viewSort: { + flatEntityToCreate: [], + flatEntityToDelete: [existingViewSortToDelete], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while destroying view sort', + ); + } + + return fromFlatViewSortToViewSortDto(existingFlatViewSort); + } + async findByWorkspaceId(workspaceId: string): Promise { return this.viewSortRepository.find({ where: { @@ -66,148 +339,4 @@ export class ViewSortService { return viewSort || null; } - - async create(viewSortData: Partial): Promise { - if (!isDefined(viewSortData.workspaceId)) { - throw new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED, - ), - ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, - { - userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage( - ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED, - ), - }, - ); - } - - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { - workspaceId: viewSortData.workspaceId, - }, - ); - - if (!isDefined(viewSortData.viewId)) { - throw new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.VIEW_ID_REQUIRED, - ), - ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, - { - userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage( - ViewSortExceptionMessageKey.VIEW_ID_REQUIRED, - ), - }, - ); - } - - if (!isDefined(viewSortData.fieldMetadataId)) { - throw new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED, - ), - ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, - { - userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage( - ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED, - ), - }, - ); - } - - const viewSort = this.viewSortRepository.create({ - ...viewSortData, - universalIdentifier: v4(), - applicationId: workspaceCustomFlatApplication.id, - }); - - const savedViewSort = await this.viewSortRepository.save(viewSort); - - await this.flushGraphQLCache(viewSortData.workspaceId); - - return savedViewSort; - } - - async update( - id: string, - workspaceId: string, - updateData: Partial, - ): Promise { - const existingViewSort = await this.findById(id, workspaceId); - - if (!isDefined(existingViewSort)) { - throw new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - id, - ), - ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, - ); - } - - const updatedViewSort = await this.viewSortRepository.save({ - id, - ...updateData, - }); - - await this.flushGraphQLCache(workspaceId); - - return { ...existingViewSort, ...updatedViewSort }; - } - - async delete(id: string, workspaceId: string): Promise { - const viewSort = await this.findById(id, workspaceId); - - if (!isDefined(viewSort)) { - throw new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - id, - ), - ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, - ); - } - - await this.viewSortRepository.softDelete(id); - - await this.flushGraphQLCache(workspaceId); - - return viewSort; - } - - async destroy(id: string, workspaceId: string): Promise { - const viewSort = await this.viewSortRepository.findOne({ - where: { - id, - workspaceId, - }, - relations: ['workspace', 'view'], - withDeleted: true, - }); - - if (!isDefined(viewSort)) { - throw new ViewSortException( - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - id, - ), - ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, - ); - } - - await this.viewSortRepository.delete(id); - - await this.flushGraphQLCache(workspaceId); - - return true; - } - - private async flushGraphQLCache(workspaceId: string): Promise { - await this.workspaceCacheStorageService.flushGraphQLOperation({ - operationName: FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION, - workspaceId, - }); - } } diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/utils/from-flat-view-sort-to-view-sort-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/utils/from-flat-view-sort-to-view-sort-dto.util.ts new file mode 100644 index 0000000000..ad0910fc6d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/utils/from-flat-view-sort-to-view-sort-dto.util.ts @@ -0,0 +1,15 @@ +import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type'; +import { type ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto'; + +export const fromFlatViewSortToViewSortDto = ( + flatViewSort: FlatViewSort, +): ViewSortDTO => { + const { createdAt, updatedAt, deletedAt, ...rest } = flatViewSort; + + return { + ...rest, + createdAt: new Date(createdAt), + updatedAt: new Date(updatedAt), + deletedAt: deletedAt ? new Date(deletedAt) : null, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/view-sort.module.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/view-sort.module.ts index 96fd7fa04b..16251ee985 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/view-sort.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/view-sort.module.ts @@ -10,14 +10,18 @@ import { ViewSortResolver } from 'src/engine/metadata-modules/view-sort/resolver import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; @Module({ imports: [ TypeOrmModule.forFeature([ViewSortEntity, ViewEntity]), - PermissionsModule, WorkspaceCacheStorageModule, - ViewPermissionsModule, ApplicationModule, + PermissionsModule, + WorkspaceMigrationModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, + ViewPermissionsModule, ], controllers: [ViewSortController], providers: [ViewSortService, ViewSortResolver], 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 aa7af0b8ff..b03497a8a7 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 @@ -175,6 +175,8 @@ describe('WorkspaceEntityManager', () => { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortIds: [], + viewSortUniversalIdentifiers: [], universalSettings: null, }; 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 d4466da527..5d6f84f5b4 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 @@ -105,6 +105,8 @@ describe('WorkspaceRepository', () => { kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortIds: [], + viewSortUniversalIdentifiers: [], universalSettings: null, }; diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts index c88b8a8725..1ecd1c30de 100644 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts @@ -21,6 +21,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = { flatViewGroupMaps: 'flat-maps:view-group', flatViewFilterMaps: 'flat-maps:view-filter', flatViewFilterGroupMaps: 'flat-maps:view-filter-group', + flatViewSortMaps: 'flat-maps:view-sort', flatLogicFunctionMaps: 'flat-maps:logic-function', featureFlagsMap: 'feature-flag:feature-flags-map', rolesPermissions: 'metadata:permissions:roles-permissions', diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util.ts index 0977ea7016..c2b12dabd9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util.ts @@ -110,6 +110,8 @@ export const createStandardFieldFlatMetadata = < kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortIds: [], + viewSortUniversalIdentifiers: [], universalSettings: settings ?? null, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util.ts index 32f52bc973..3f71948a97 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util.ts @@ -130,6 +130,8 @@ export const createStandardRelationFieldFlatMetadata = < kanbanAggregateOperationViewUniversalIdentifiers: [], calendarViewUniversalIdentifiers: [], mainGroupByFieldMetadataViewUniversalIdentifiers: [], + viewSortIds: [], + viewSortUniversalIdentifiers: [], universalSettings: settings, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util.ts index ed6b90489c..d29db4773d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util.ts @@ -152,6 +152,8 @@ export const createStandardViewFlatMetadata = < viewGroupUniversalIdentifiers: [], viewFilterGroupIds: [], viewFilterGroupUniversalIdentifiers: [], + viewSortIds: [], + viewSortUniversalIdentifiers: [], createdAt: now, updatedAt: now, deletedAt: null, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts index 3c8da25881..015b0cb61c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts @@ -166,6 +166,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ case 'commandMenuItem': case 'navigationMenuItem': case 'frontComponent': + case 'viewSort': case 'webhook': { if (!isDefined(idByUniversalIdentifier)) { return action; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts index b82543b293..5222fb0e5b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts @@ -36,6 +36,7 @@ import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/wo import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service'; import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service'; import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service'; +import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service'; @Injectable() export class WorkspaceMigrationBuildOrchestratorService { @@ -48,6 +49,7 @@ export class WorkspaceMigrationBuildOrchestratorService { private readonly workspaceMigrationViewFilterGroupActionsBuilderService: WorkspaceMigrationViewFilterGroupActionsBuilderService, private readonly workspaceMigrationViewGroupActionsBuilderService: WorkspaceMigrationViewGroupActionsBuilderService, private readonly workspaceMigrationViewFieldGroupActionsBuilderService: WorkspaceMigrationViewFieldGroupActionsBuilderService, + private readonly workspaceMigrationViewSortActionsBuilderService: WorkspaceMigrationViewSortActionsBuilderService, private readonly workspaceMigrationLogicFunctionActionsBuilderService: WorkspaceMigrationLogicFunctionActionsBuilderService, private readonly workspaceMigrationRoleTargetActionsBuilderService: WorkspaceMigrationRoleTargetActionsBuilderService, private readonly workspaceMigrationFieldActionsBuilderService: WorkspaceMigrationFieldActionsBuilderService, @@ -134,6 +136,7 @@ export class WorkspaceMigrationBuildOrchestratorService { flatViewFilterGroupMaps, flatViewGroupMaps, flatViewFieldGroupMaps, + flatViewSortMaps, flatRowLevelPermissionPredicateMaps, flatRowLevelPermissionPredicateGroupMaps, flatRoleMaps, @@ -355,6 +358,28 @@ export class WorkspaceMigrationBuildOrchestratorService { } } + if (isDefined(flatViewSortMaps)) { + const { from: fromFlatViewSortMaps, to: toFlatViewSortMaps } = + flatViewSortMaps; + const viewSortResult = + await this.workspaceMigrationViewSortActionsBuilderService.validateAndBuild( + { + additionalCacheDataMaps, + from: fromFlatViewSortMaps, + to: toFlatViewSortMaps, + buildOptions, + dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps, + workspaceId, + }, + ); + + if (viewSortResult.status === 'fail') { + orchestratorFailureReport.viewSort.push(...viewSortResult.errors); + } else { + orchestratorActionsReport.viewSort = viewSortResult.actions; + } + } + if (isDefined(flatRowLevelPermissionPredicateGroupMaps)) { const { from: fromFlatRowLevelPermissionPredicateGroupMaps, @@ -763,6 +788,9 @@ export class WorkspaceMigrationBuildOrchestratorService { ...aggregatedOrchestratorActionsReport.viewGroup.delete, ...aggregatedOrchestratorActionsReport.viewGroup.create, ...aggregatedOrchestratorActionsReport.viewGroup.update, + ...aggregatedOrchestratorActionsReport.viewSort.create, + ...aggregatedOrchestratorActionsReport.viewSort.update, + ...aggregatedOrchestratorActionsReport.viewSort.delete, /// // Logic functions diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap index d1aa610ce9..c6a76ad172 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/__tests__/__snapshots__/all-universal-flat-entity-foreign-key-aggregator-properties.constant.spec.ts.snap @@ -5,6 +5,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc "agent": [], "commandMenuItem": [], "fieldMetadata": [ + "viewSortUniversalIdentifiers", "viewFieldUniversalIdentifiers", "viewFilterUniversalIdentifiers", "kanbanAggregateOperationViewUniversalIdentifiers", @@ -56,6 +57,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc "viewFilterUniversalIdentifiers", ], "viewGroup": [], + "viewSort": [], "webhook": [], } `; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts index 37415db4a8..760ef0aa9b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts @@ -18,6 +18,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = { viewFieldGroup: {}, viewFilter: {}, viewFilterGroup: {}, + viewSort: {}, index: {}, role: {}, roleTarget: {}, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type.ts new file mode 100644 index 0000000000..25693bbf10 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type.ts @@ -0,0 +1,7 @@ +import { type ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; +import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type'; + +export type UniversalFlatViewSort = UniversalFlatEntityFrom< + ViewSortEntity, + 'viewSort' +>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type.ts index 2fadc95c06..5cf639f509 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type.ts @@ -1,8 +1,4 @@ import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type'; -// TODO remove once viewSorts has been migrated to v2 -export type UniversalFlatView = UniversalFlatEntityFrom< - Omit, - 'view' ->; +export type UniversalFlatView = UniversalFlatEntityFrom; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/__snapshots__/universal-flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/__snapshots__/universal-flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap index 823161bdd1..b21014090b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/__snapshots__/universal-flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/__tests__/__snapshots__/universal-flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap @@ -45,6 +45,8 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a crea "viewFieldUniversalIdentifiers": [], "viewFilterIds": [], "viewFilterUniversalIdentifiers": [], + "viewSortIds": [], + "viewSortUniversalIdentifiers": [], "workspaceId": "workspace-id-1", }, }, @@ -106,6 +108,8 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a dele "viewFieldUniversalIdentifiers": [], "viewFilterIds": [], "viewFilterUniversalIdentifiers": [], + "viewSortIds": [], + "viewSortUniversalIdentifiers": [], "workspaceId": "workspace-id-1", }, }, @@ -181,6 +185,8 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create "viewFieldUniversalIdentifiers": [], "viewFilterIds": [], "viewFilterUniversalIdentifiers": [], + "viewSortIds": [], + "viewSortUniversalIdentifiers": [], "workspaceId": "workspace-id-1", }, }, @@ -228,6 +234,8 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create "viewFieldUniversalIdentifiers": [], "viewFilterIds": [], "viewFilterUniversalIdentifiers": [], + "viewSortIds": [], + "viewSortUniversalIdentifiers": [], "workspaceId": "workspace-id-1", }, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type.ts new file mode 100644 index 0000000000..4dca59bbb4 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type.ts @@ -0,0 +1,24 @@ +import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type'; +import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type'; +import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type'; +import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type'; +import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type'; +import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type'; + +export type FlatCreateViewSortAction = + BaseFlatCreateWorkspaceMigrationAction<'viewSort'>; + +export type FlatUpdateViewSortAction = + BaseFlatUpdateWorkspaceMigrationAction<'viewSort'>; + +export type FlatDeleteViewSortAction = + BaseFlatDeleteWorkspaceMigrationAction<'viewSort'>; + +export type UniversalCreateViewSortAction = + BaseUniversalCreateWorkspaceMigrationAction<'viewSort'>; + +export type UniversalUpdateViewSortAction = + BaseUniversalUpdateWorkspaceMigrationAction<'viewSort'>; + +export type UniversalDeleteViewSortAction = + BaseUniversalDeleteWorkspaceMigrationAction<'viewSort'>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service.ts new file mode 100644 index 0000000000..55cf8050cb --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@nestjs/common'; + +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service'; +import { UniversalUpdateViewSortAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type'; +import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service'; +import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; +import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type'; + +@Injectable() +export class WorkspaceMigrationViewSortActionsBuilderService extends WorkspaceEntityMigrationBuilderService< + typeof ALL_METADATA_NAME.viewSort +> { + constructor( + private readonly flatViewSortValidatorService: FlatViewSortValidatorService, + ) { + super(ALL_METADATA_NAME.viewSort); + } + + protected validateFlatEntityCreation( + args: UniversalFlatEntityValidationArgs, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.viewSort, + 'create' + > { + const validationResult = + this.flatViewSortValidatorService.validateFlatViewSortCreation(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatViewSortToValidate } = args; + + return { + status: 'success', + action: { + type: 'create', + metadataName: 'viewSort', + flatEntity: flatViewSortToValidate, + }, + }; + } + + protected validateFlatEntityDeletion( + args: UniversalFlatEntityValidationArgs, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.viewSort, + 'delete' + > { + const validationResult = + this.flatViewSortValidatorService.validateFlatViewSortDeletion(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { flatEntityToValidate: flatViewSortToValidate } = args; + + return { + status: 'success', + action: { + type: 'delete', + metadataName: 'viewSort', + universalIdentifier: flatViewSortToValidate.universalIdentifier, + }, + }; + } + + protected validateFlatEntityUpdate( + args: FlatEntityUpdateValidationArgs, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.viewSort, + 'update' + > { + const validationResult = + this.flatViewSortValidatorService.validateFlatViewSortUpdate(args); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { universalIdentifier, flatEntityUpdate } = args; + + const updateViewSortAction: UniversalUpdateViewSortAction = { + type: 'update', + metadataName: 'viewSort', + universalIdentifier, + update: flatEntityUpdate, + }; + + return { + status: 'success', + action: updateViewSortAction, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service.ts new file mode 100644 index 0000000000..a0597f175b --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service.ts @@ -0,0 +1,229 @@ +import { Injectable } from '@nestjs/common'; + +import { msg, t } from '@lingui/core/macro'; +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; + +import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; +import { ViewSortExceptionCode } from 'src/engine/metadata-modules/view-sort/exceptions/view-sort.exception'; +import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; +import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util'; +import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type'; +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; +import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; +import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util'; + +@Injectable() +export class FlatViewSortValidatorService { + constructor() {} + + validateFlatViewSortCreation({ + flatEntityToValidate: flatViewSortToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatViewSortMaps: optimisticFlatViewSortMaps, + flatViewMaps, + flatFieldMetadataMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.viewSort + >): FailedFlatEntityValidation<'viewSort', 'create'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier: flatViewSortToValidate.universalIdentifier, + viewUniversalIdentifier: flatViewSortToValidate.viewUniversalIdentifier, + }, + metadataName: 'viewSort', + type: 'create', + }); + + const existingFlatViewSort = findFlatEntityByUniversalIdentifier({ + universalIdentifier: flatViewSortToValidate.universalIdentifier, + flatEntityMaps: optimisticFlatViewSortMaps, + }); + + if (isDefined(existingFlatViewSort)) { + const flatViewSortId = flatViewSortToValidate.universalIdentifier; + + validationResult.errors.push({ + code: ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, + message: t`View sort with id ${flatViewSortId} already exists`, + userFriendlyMessage: msg`View sort already exists`, + }); + } + + const flatFieldMetadata = findFlatEntityByUniversalIdentifier({ + universalIdentifier: + flatViewSortToValidate.fieldMetadataUniversalIdentifier, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (!isDefined(flatFieldMetadata)) { + validationResult.errors.push({ + code: ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, + message: t`Field metadata not found`, + userFriendlyMessage: msg`Field metadata not found`, + }); + } + + const flatView = findFlatEntityByUniversalIdentifier({ + universalIdentifier: flatViewSortToValidate.viewUniversalIdentifier, + flatEntityMaps: flatViewMaps, + }); + + if (!isDefined(flatView)) { + validationResult.errors.push({ + code: ViewSortExceptionCode.VIEW_NOT_FOUND, + message: t`View not found`, + userFriendlyMessage: msg`View not found`, + }); + + return validationResult; + } + + const otherFlatViewSorts = + findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow({ + universalIdentifiers: flatView.viewSortUniversalIdentifiers, + flatEntityMaps: optimisticFlatViewSortMaps, + }); + + const equivalentExistingFlatViewSortExists = otherFlatViewSorts.some( + (flatViewSort) => + flatViewSort.universalIdentifier === + flatViewSortToValidate.universalIdentifier && + flatViewSort.fieldMetadataUniversalIdentifier === + flatViewSortToValidate.fieldMetadataUniversalIdentifier, + ); + + if (equivalentExistingFlatViewSortExists) { + validationResult.errors.push({ + code: ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, + message: t`View sort with same fieldMetadataId and viewId already exists`, + userFriendlyMessage: msg`View sort already exists`, + }); + } + + if ( + !isDefined(flatViewSortToValidate.direction) || + !Object.values(ViewSortDirection).includes( + flatViewSortToValidate.direction, + ) + ) { + validationResult.errors.push({ + code: ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, + message: t`View sort with invalid direction`, + userFriendlyMessage: msg`View sort with invalid direction, should be ASC or DESC`, + }); + } + + return validationResult; + } + + public validateFlatViewSortUpdate({ + universalIdentifier, + flatEntityUpdate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatViewSortMaps: optimisticFlatViewSortMaps, + flatViewMaps, + }, + }: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.viewSort + >): FailedFlatEntityValidation<'viewSort', 'update'> { + const existingFlatViewSort = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatViewSortMaps, + }); + + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'viewSort', + type: 'update', + }); + + if (!isDefined(existingFlatViewSort)) { + validationResult.errors.push({ + code: ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, + message: t`View sort to update not found`, + userFriendlyMessage: msg`View sort to update not found`, + }); + + return validationResult; + } + + const updatedFlatViewSort = { + ...existingFlatViewSort, + ...flatEntityUpdate, + }; + + if ( + !isDefined(updatedFlatViewSort?.direction) || + !( + updatedFlatViewSort.direction === ViewSortDirection.DESC || + updatedFlatViewSort.direction === ViewSortDirection.ASC + ) + ) { + validationResult.errors.push({ + code: ViewSortExceptionCode.INVALID_VIEW_SORT_DATA, + message: t`Correct direction is required`, + userFriendlyMessage: msg`Correct direction is required`, + }); + } + + validationResult.flatEntityMinimalInformation = { + ...validationResult.flatEntityMinimalInformation, + viewUniversalIdentifier: updatedFlatViewSort.viewUniversalIdentifier, + }; + + const flatView = findFlatEntityByUniversalIdentifier({ + universalIdentifier: updatedFlatViewSort.viewUniversalIdentifier, + flatEntityMaps: flatViewMaps, + }); + + if (!isDefined(flatView)) { + validationResult.errors.push({ + code: ViewSortExceptionCode.VIEW_NOT_FOUND, + message: t`View sort to update parent view not found`, + userFriendlyMessage: msg`View sort to update parent view not found`, + }); + + return validationResult; + } + + return validationResult; + } + + public validateFlatViewSortDeletion({ + flatEntityToValidate: { universalIdentifier }, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatViewSortMaps: optimisticFlatViewSortMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.viewSort + >): FailedFlatEntityValidation<'viewSort', 'delete'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'viewSort', + type: 'delete', + }); + + const existingFlatViewSort = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatViewSortMaps, + }); + + if (!isDefined(existingFlatViewSort)) { + validationResult.errors.push({ + code: ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, + message: t`View sort to delete not found`, + userFriendlyMessage: msg`View sort to delete not found`, + }); + + return validationResult; + } + + return validationResult; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts index fa90309ad3..ea8310d6ef 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts @@ -26,6 +26,7 @@ import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/wor import { FlatViewGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-group-validator.service'; import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-validator.service'; import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service'; +import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service'; @Module({ imports: [FeatureFlagModule], @@ -36,6 +37,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp FlatViewFilterGroupValidatorService, FlatViewGroupValidatorService, FlatViewFieldGroupValidatorService, + FlatViewSortValidatorService, FlatIndexValidatorService, FlatFieldMetadataValidatorService, FlatObjectMetadataValidatorService, @@ -63,6 +65,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp FlatViewFilterGroupValidatorService, FlatViewGroupValidatorService, FlatViewFieldGroupValidatorService, + FlatViewSortValidatorService, FlatIndexValidatorService, FlatFieldMetadataValidatorService, FlatObjectMetadataValidatorService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts index 30d1a3e04c..b2966f1c52 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts @@ -26,6 +26,7 @@ import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/wo import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service'; import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service'; import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module'; +import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service'; @Module({ imports: [FeatureFlagModule, WorkspaceMigrationBuilderValidatorsModule], @@ -38,6 +39,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationViewFilterGroupActionsBuilderService, WorkspaceMigrationViewGroupActionsBuilderService, WorkspaceMigrationViewFieldGroupActionsBuilderService, + WorkspaceMigrationViewSortActionsBuilderService, WorkspaceMigrationIndexActionsBuilderService, WorkspaceMigrationLogicFunctionActionsBuilderService, WorkspaceMigrationFieldActionsBuilderService, @@ -64,6 +66,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationViewFilterGroupActionsBuilderService, WorkspaceMigrationViewGroupActionsBuilderService, WorkspaceMigrationViewFieldGroupActionsBuilderService, + WorkspaceMigrationViewSortActionsBuilderService, WorkspaceMigrationFieldActionsBuilderService, WorkspaceMigrationLogicFunctionActionsBuilderService, WorkspaceMigrationRoleActionsBuilderService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util.ts index 7be53bb9b0..ee8558720d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util.ts @@ -133,6 +133,7 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({ calendarViewIds: [], mainGroupByFieldMetadataViewIds: [], kanbanAggregateOperationViewIds: [], + viewSortIds: [], ...emptyUniversalForeignKeyAggregators, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/create-view-sort-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/create-view-sort-action-handler.service.ts new file mode 100644 index 0000000000..2f2cd350f0 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/create-view-sort-action-handler.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; + +import { v4 } from 'uuid'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { + FlatCreateViewSortAction, + UniversalCreateViewSortAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; +import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; + +@Injectable() +export class CreateViewSortActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create', + 'viewSort', +) { + override async transpileUniversalActionToFlatAction({ + action, + allFlatEntityMaps, + flatApplication, + workspaceId, + }: WorkspaceMigrationActionRunnerArgs): Promise { + const { fieldMetadataId, viewId } = + resolveUniversalRelationIdentifiersToIds({ + flatEntityMaps: allFlatEntityMaps, + metadataName: action.metadataName, + universalForeignKeyValues: action.flatEntity, + }); + + return { + ...action, + flatEntity: { + ...action.flatEntity, + fieldMetadataId, + viewId, + id: action.id ?? v4(), + applicationId: flatApplication.id, + workspaceId, + }, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner } = context; + const { flatEntity } = flatAction; + + await this.insertFlatEntitiesInRepository({ + queryRunner, + flatEntities: [flatEntity], + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/delete-view-sort-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/delete-view-sort-action-handler.service.ts new file mode 100644 index 0000000000..f80074333e --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/delete-view-sort-action-handler.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; +import { + FlatDeleteViewSortAction, + UniversalDeleteViewSortAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteViewSortActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete', + 'viewSort', +) { + constructor() { + super(); + } + + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return this.transpileUniversalDeleteActionToFlatDeleteAction(context); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + + const viewSortRepository = + queryRunner.manager.getRepository(ViewSortEntity); + + await viewSortRepository.delete({ + id: flatAction.entityId, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/update-view-sort-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/update-view-sort-action-handler.service.ts new file mode 100644 index 0000000000..3b80baa82d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/update-view-sort-action-handler.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; +import { + FlatUpdateViewSortAction, + UniversalUpdateViewSortAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util'; + +@Injectable() +export class UpdateViewSortActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update', + 'viewSort', +) { + constructor() { + super(); + } + + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, allFlatEntityMaps } = context; + + const flatViewSort = findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: allFlatEntityMaps.flatViewSortMaps, + universalIdentifier: action.universalIdentifier, + }); + + const update = resolveUniversalUpdateRelationIdentifiersToIds({ + metadataName: 'viewSort', + universalUpdate: action.update, + allFlatEntityMaps, + }); + + return { + type: 'update', + metadataName: 'viewSort', + entityId: flatViewSort.id, + update, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + const { entityId, update } = flatAction; + + const viewSortRepository = + queryRunner.manager.getRepository(ViewSortEntity); + + await viewSortRepository.update({ id: entityId, workspaceId }, update); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts index c1ec8dd36a..496443bb5e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts @@ -62,6 +62,7 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi viewGroupIds: [], viewFilterIds: [], viewFilterGroupIds: [], + viewSortIds: [], ...emptyUniversalForeignKeyAggregators, }, }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts index b87d4b0f58..66ad69c91f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts @@ -72,6 +72,9 @@ import { UpdateViewGroupActionHandlerService } from 'src/engine/workspace-manage import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service'; import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/delete-view-action-handler.service'; import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/update-view-action-handler.service'; +import { CreateViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/create-view-sort-action-handler.service'; +import { UpdateViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/update-view-sort-action-handler.service'; +import { DeleteViewSortActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-sort/services/delete-view-sort-action-handler.service'; @Module({ imports: [ @@ -107,6 +110,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor UpdateViewFilterGroupActionHandlerService, DeleteViewFilterGroupActionHandlerService, + CreateViewSortActionHandlerService, + UpdateViewSortActionHandlerService, + DeleteViewSortActionHandlerService, + CreateViewGroupActionHandlerService, UpdateViewGroupActionHandlerService, DeleteViewGroupActionHandlerService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts index 2a91a24e03..4b626e2d94 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/metadata-event-to-emit.constant.ts @@ -23,6 +23,6 @@ export const METADATA_EVENTS_TO_EMIT = { rowLevelPermissionPredicateGroup: true, index: true, logicFunction: true, - + viewSort: true, webhook: false, } as const satisfies { [P in AllMetadataName]: boolean }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts index 96bb32ff61..209c56acb2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts @@ -89,6 +89,7 @@ const deriveAllMetadataEventsFromCreateAction = ( case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'viewSort': case 'webhook': { return [ { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts index 48be453b13..1f6078b1b5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts @@ -53,6 +53,7 @@ const deriveAllMetadataEventsFromDeleteAction = ({ case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'viewSort': case 'webhook': { const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow< MetadataFlatEntity diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts index accc98b81e..a325a25764 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts @@ -131,6 +131,7 @@ const deriveAllMetadataEventsFromUpdateAction = ({ case 'commandMenuItem': case 'frontComponent': case 'navigationMenuItem': + case 'viewSort': case 'webhook': { const flatEntityMapsKey = getMetadataFlatEntityMapsKey( flatAction.metadataName, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts index 69d3fbedc2..0fc1b9ceb8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts @@ -51,6 +51,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({ case 'viewField': case 'viewGroup': case 'viewFieldGroup': + case 'viewSort': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts index a270d0c1c5..73f4224901 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts @@ -23,6 +23,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({ case 'viewField': case 'viewGroup': case 'viewFieldGroup': + case 'viewSort': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts index e143edab4e..6f5f20f475 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts @@ -45,6 +45,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({ case 'viewField': case 'viewGroup': case 'viewFieldGroup': + case 'viewSort': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts b/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts index 40844cff7a..d4e60153a6 100644 --- a/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts +++ b/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts @@ -26,6 +26,7 @@ export const getMockFieldMetadataEntity = < viewFilters: [], viewFields: [], kanbanAggregateOperationViews: [], + viewSorts: [], morphId: null, fieldPermissions: [], icon: null, diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-sort-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-sort-resolver.integration-spec.ts index 04b03882cd..7be88a77a5 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-sort-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-sort-resolver.integration-spec.ts @@ -213,7 +213,6 @@ describe('View Sort Resolver', () => { ErrorCode.NOT_FOUND, generateViewSortExceptionMessage( ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_SORT_ID, ), ); }); @@ -250,7 +249,6 @@ describe('View Sort Resolver', () => { ErrorCode.NOT_FOUND, generateViewSortExceptionMessage( ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_SORT_ID, ), ); }); @@ -287,7 +285,6 @@ describe('View Sort Resolver', () => { ErrorCode.NOT_FOUND, generateViewSortExceptionMessage( ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_SORT_ID, ), ); }); diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-view-sort-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-view-sort-operation-factory.util.ts index 906893818b..a747e6c0f7 100644 --- a/packages/twenty-server/test/integration/graphql/utils/delete-view-sort-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/delete-view-sort-operation-factory.util.ts @@ -6,11 +6,11 @@ export const deleteViewSortOperationFactory = ({ viewSortId: string; }) => ({ query: gql` - mutation DeleteCoreViewSort($id: String!) { - deleteCoreViewSort(id: $id) + mutation DeleteCoreViewSort($input: DeleteViewSortInput!) { + deleteCoreViewSort(input: $input) } `, variables: { - id: viewSortId, + input: { id: viewSortId }, }, }); diff --git a/packages/twenty-server/test/integration/graphql/utils/destroy-view-sort-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/destroy-view-sort-operation-factory.util.ts index 4422072e5a..8e6be1e5d6 100644 --- a/packages/twenty-server/test/integration/graphql/utils/destroy-view-sort-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/destroy-view-sort-operation-factory.util.ts @@ -6,11 +6,11 @@ export const destroyViewSortOperationFactory = ({ viewSortId: string; }) => ({ query: gql` - mutation DestroyCoreViewSort($id: String!) { - destroyCoreViewSort(id: $id) + mutation DestroyCoreViewSort($input: DestroyViewSortInput!) { + destroyCoreViewSort(input: $input) } `, variables: { - id: viewSortId, + input: { id: viewSortId }, }, }); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-view-sort-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-view-sort-operation-factory.util.ts index b7a0a3c189..e529c30adf 100644 --- a/packages/twenty-server/test/integration/graphql/utils/update-view-sort-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/update-view-sort-operation-factory.util.ts @@ -11,14 +11,13 @@ export const updateViewSortOperationFactory = ({ data?: object; }) => ({ query: gql` - mutation UpdateCoreViewSort($id: String!, $input: UpdateViewSortInput!) { - updateCoreViewSort(id: $id, input: $input) { + mutation UpdateCoreViewSort($input: UpdateViewSortInput!) { + updateCoreViewSort(input: $input) { ${gqlFields} } } `, variables: { - id: viewSortId, - input: data, + input: { id: viewSortId, update: data }, }, }); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-creation.integration-spec.ts.snap new file mode 100644 index 0000000000..b94dbb7c79 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-creation.integration-spec.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Sort creation should fail when fieldMetadataId does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Could not find fieldMetadata for given fieldMetadataId", + "name": "NotFoundError", +} +`; + +exports[`View Sort creation should fail when viewId does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Could not find fieldMetadata for given fieldMetadataId", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-deletion.integration-spec.ts.snap new file mode 100644 index 0000000000..aeb912d450 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-deletion.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Sort deletion should fail when viewSortId does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "View sort not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-destroy.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-destroy.integration-spec.ts.snap new file mode 100644 index 0000000000..a87331133e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-destroy.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Sort destroy should fail when viewSortId does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "View sort not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-update.integration-spec.ts.snap new file mode 100644 index 0000000000..fafa61883a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/__snapshots__/failing-view-sort-update.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Sort update should fail when viewSortId does not exist 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "View sort not found", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-creation.integration-spec.ts new file mode 100644 index 0000000000..71e33e4292 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-creation.integration-spec.ts @@ -0,0 +1,108 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { createOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/create-one-core-view-sort.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { v4 } from 'uuid'; +import { ViewType } from 'twenty-shared/types'; + +import { type CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input'; +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; + +type TestSetup = { + createdViewId: string; +}; + +type TestContext = { + input: (testSetup: TestSetup) => CreateViewSortInput; +}; + +const failingViewSortCreationTestCases: EachTestingContext[] = [ + { + title: 'when viewId does not exist', + context: { + input: () => ({ + viewId: v4(), + fieldMetadataId: v4(), + direction: ViewSortDirection.ASC, + }), + }, + }, + { + title: 'when fieldMetadataId does not exist', + context: { + input: (testSetup) => ({ + viewId: testSetup.createdViewId, + fieldMetadataId: v4(), + direction: ViewSortDirection.ASC, + }), + }, + }, +]; + +describe('View Sort creation should fail', () => { + let createdViewId: string; + + beforeAll(async () => { + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: {}, + paging: { first: 1000 }, + }, + gqlFields: ` + id + nameSingular + `, + }); + + jestExpectToBeDefined(objects); + + const companyObjectMetadata = objects.find( + (object: { nameSingular: string }) => object.nameSingular === 'company', + ); + + jestExpectToBeDefined(companyObjectMetadata); + + const { data: viewData } = await createOneCoreView({ + expectToFail: false, + input: { + name: 'Test View For Failing View Sort Creation', + objectMetadataId: companyObjectMetadata.id, + type: ViewType.TABLE, + icon: 'IconBuildingSkyscraper', + }, + }); + + createdViewId = viewData?.createCoreView?.id; + jestExpectToBeDefined(createdViewId); + }); + + afterAll(async () => { + if (createdViewId) { + await destroyOneCoreView({ + expectToFail: false, + viewId: createdViewId, + }); + } + }); + + it.each(eachTestingContextFilter(failingViewSortCreationTestCases))( + '$title', + async ({ context }) => { + const { errors } = await createOneCoreViewSort({ + expectToFail: true, + input: context.input({ createdViewId }), + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-deletion.integration-spec.ts new file mode 100644 index 0000000000..ed75d493a8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-deletion.integration-spec.ts @@ -0,0 +1,36 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { deleteOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/delete-one-core-view-sort.util'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { v4 } from 'uuid'; + +type TestContext = { + id: string; +}; + +const failingViewSortDeletionTestCases: EachTestingContext[] = [ + { + title: 'when viewSortId does not exist', + context: { + id: v4(), + }, + }, +]; + +describe('View Sort deletion should fail', () => { + it.each(eachTestingContextFilter(failingViewSortDeletionTestCases))( + '$title', + async ({ context }) => { + const { errors } = await deleteOneCoreViewSort({ + input: { id: context.id }, + expectToFail: true, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-destroy.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-destroy.integration-spec.ts new file mode 100644 index 0000000000..ee2f56a9fb --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-destroy.integration-spec.ts @@ -0,0 +1,36 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { v4 } from 'uuid'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { destroyOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/destroy-one-core-view-sort.util'; + +type TestContext = { + id: string; +}; + +const failingViewSortDestroyTestCases: EachTestingContext[] = [ + { + title: 'when viewSortId does not exist', + context: { + id: v4(), + }, + }, +]; + +describe('View Sort destroy should fail', () => { + it.each(eachTestingContextFilter(failingViewSortDestroyTestCases))( + '$title', + async ({ context }) => { + const { errors } = await destroyOneCoreViewSort({ + input: { id: context.id }, + expectToFail: true, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-update.integration-spec.ts new file mode 100644 index 0000000000..0bad4b94f1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/failing-view-sort-update.integration-spec.ts @@ -0,0 +1,43 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { updateOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/update-one-core-view-sort.util'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { v4 } from 'uuid'; + +import { type UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input'; + +type TestContext = { + input: UpdateViewSortInput; +}; + +const failingViewSortUpdateTestCases: EachTestingContext[] = [ + { + title: 'when viewSortId does not exist', + context: { + input: { + id: v4(), + update: { + direction: undefined, + }, + }, + }, + }, +]; + +describe('View Sort update should fail', () => { + it.each(eachTestingContextFilter(failingViewSortUpdateTestCases))( + '$title', + async ({ context }) => { + const { errors } = await updateOneCoreViewSort({ + expectToFail: true, + input: context.input, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-creation.integration-spec.ts new file mode 100644 index 0000000000..c17844f916 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-creation.integration-spec.ts @@ -0,0 +1,156 @@ +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 { createOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/create-one-core-view-sort.util'; +import { destroyOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/destroy-one-core-view-sort.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { FieldMetadataType, ViewType } from 'twenty-shared/types'; + +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; + +describe('View Sort creation should succeed', () => { + let testObjectMetadataId: string; + let testFieldMetadataId: string; + let createdViewId: string; + let createdViewSortId: string; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'testViewSortCreationObject', + namePlural: 'testViewSortCreationObjects', + labelSingular: 'Test View Sort Creation Object', + labelPlural: 'Test View Sort Creation Objects', + icon: 'IconSort', + }, + }); + + testObjectMetadataId = objectMetadataId; + + const { + data: { + createOneField: { id: fieldMetadataId }, + }, + } = await createOneFieldMetadata({ + input: { + name: 'testSortField', + label: 'Test Sort Field', + type: FieldMetadataType.TEXT, + objectMetadataId: testObjectMetadataId, + isLabelSyncedWithName: true, + }, + gqlFields: 'id name label', + }); + + testFieldMetadataId = fieldMetadataId; + + const { data: viewData } = await createOneCoreView({ + expectToFail: false, + input: { + name: 'Test View For View Sort Creation', + objectMetadataId: testObjectMetadataId, + type: ViewType.TABLE, + icon: 'IconBuildingSkyscraper', + }, + }); + + createdViewId = viewData?.createCoreView?.id; + jestExpectToBeDefined(createdViewId); + }); + + afterAll(async () => { + if (createdViewId) { + await destroyOneCoreView({ + expectToFail: false, + viewId: createdViewId, + }); + } + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: testObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + await deleteOneObjectMetadata({ + input: { idToDelete: testObjectMetadataId }, + }); + }); + + afterEach(async () => { + if (createdViewSortId) { + await destroyOneCoreViewSort({ + expectToFail: false, + input: { id: createdViewSortId }, + }); + createdViewSortId = ''; + } + }); + + it('should create a view sort with default direction (ASC)', async () => { + const { data } = await createOneCoreViewSort({ + expectToFail: false, + input: { + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + }, + }); + + createdViewSortId = data?.createCoreViewSort?.id; + + expect(data.createCoreViewSort).toMatchObject({ + id: expect.any(String), + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + direction: ViewSortDirection.ASC, + }); + }); + + it('should create a view sort with ASC direction', async () => { + const { data } = await createOneCoreViewSort({ + expectToFail: false, + input: { + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + direction: ViewSortDirection.ASC, + }, + }); + + createdViewSortId = data?.createCoreViewSort?.id; + + expect(data.createCoreViewSort).toMatchObject({ + id: expect.any(String), + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + direction: ViewSortDirection.ASC, + }); + }); + + it('should create a view sort with DESC direction', async () => { + const { data } = await createOneCoreViewSort({ + expectToFail: false, + input: { + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + direction: ViewSortDirection.DESC, + }, + }); + + createdViewSortId = data?.createCoreViewSort?.id; + + expect(data.createCoreViewSort).toMatchObject({ + id: expect.any(String), + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + direction: ViewSortDirection.DESC, + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-deletion.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-deletion.integration-spec.ts new file mode 100644 index 0000000000..02aa2a6603 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-deletion.integration-spec.ts @@ -0,0 +1,115 @@ +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 { createOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/create-one-core-view-sort.util'; +import { deleteOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/delete-one-core-view-sort.util'; +import { destroyOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/destroy-one-core-view-sort.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { FieldMetadataType, ViewType } from 'twenty-shared/types'; + +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; + +describe('View Sort deletion should succeed', () => { + let testObjectMetadataId: string; + let testFieldMetadataId: string; + let createdViewId: string; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'testViewSortDeletionObject', + namePlural: 'testViewSortDeletionObjects', + labelSingular: 'Test View Sort Deletion Object', + labelPlural: 'Test View Sort Deletion Objects', + icon: 'IconSort', + }, + }); + + testObjectMetadataId = objectMetadataId; + + const { + data: { + createOneField: { id: fieldMetadataId }, + }, + } = await createOneFieldMetadata({ + input: { + name: 'testSortDeletionField', + label: 'Test Sort Deletion Field', + type: FieldMetadataType.TEXT, + objectMetadataId: testObjectMetadataId, + isLabelSyncedWithName: true, + }, + gqlFields: 'id name label', + }); + + testFieldMetadataId = fieldMetadataId; + + const { data: viewData } = await createOneCoreView({ + expectToFail: false, + input: { + name: 'Test View For View Sort Deletion', + objectMetadataId: testObjectMetadataId, + type: ViewType.TABLE, + icon: 'IconBuildingSkyscraper', + }, + }); + + createdViewId = viewData?.createCoreView?.id; + jestExpectToBeDefined(createdViewId); + }); + + afterAll(async () => { + if (createdViewId) { + await destroyOneCoreView({ + expectToFail: false, + viewId: createdViewId, + }); + } + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: testObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + await deleteOneObjectMetadata({ + input: { idToDelete: testObjectMetadataId }, + }); + }); + + it('should soft delete a view sort', async () => { + const { data: createData } = await createOneCoreViewSort({ + expectToFail: false, + input: { + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + direction: ViewSortDirection.ASC, + }, + }); + + const viewSortId = createData?.createCoreViewSort?.id; + + jestExpectToBeDefined(viewSortId); + + const { data: deleteData } = await deleteOneCoreViewSort({ + expectToFail: false, + input: { id: viewSortId }, + }); + + expect(deleteData.deleteCoreViewSort).toBe(true); + + // Clean up + await destroyOneCoreViewSort({ + expectToFail: false, + input: { id: viewSortId }, + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-destroy.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-destroy.integration-spec.ts new file mode 100644 index 0000000000..d0a3ff00b0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-destroy.integration-spec.ts @@ -0,0 +1,108 @@ +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 { createOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/create-one-core-view-sort.util'; +import { destroyOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/destroy-one-core-view-sort.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { FieldMetadataType, ViewType } from 'twenty-shared/types'; + +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; + +describe('View Sort deletion should succeed', () => { + let testObjectMetadataId: string; + let testFieldMetadataId: string; + let createdViewId: string; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'testViewSortDeletionObject', + namePlural: 'testViewSortDeletionObjects', + labelSingular: 'Test View Sort Deletion Object', + labelPlural: 'Test View Sort Deletion Objects', + icon: 'IconSort', + }, + }); + + testObjectMetadataId = objectMetadataId; + + const { + data: { + createOneField: { id: fieldMetadataId }, + }, + } = await createOneFieldMetadata({ + input: { + name: 'testSortDeletionField', + label: 'Test Sort Deletion Field', + type: FieldMetadataType.TEXT, + objectMetadataId: testObjectMetadataId, + isLabelSyncedWithName: true, + }, + gqlFields: 'id name label', + }); + + testFieldMetadataId = fieldMetadataId; + + const { data: viewData } = await createOneCoreView({ + expectToFail: false, + input: { + name: 'Test View For View Sort Deletion', + objectMetadataId: testObjectMetadataId, + type: ViewType.TABLE, + icon: 'IconBuildingSkyscraper', + }, + }); + + createdViewId = viewData?.createCoreView?.id; + jestExpectToBeDefined(createdViewId); + }); + + afterAll(async () => { + if (createdViewId) { + await destroyOneCoreView({ + expectToFail: false, + viewId: createdViewId, + }); + } + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: testObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + await deleteOneObjectMetadata({ + input: { idToDelete: testObjectMetadataId }, + }); + }); + + it('should hard delete (destroy) a view sort', async () => { + const { data: createData } = await createOneCoreViewSort({ + expectToFail: false, + input: { + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + direction: ViewSortDirection.DESC, + }, + }); + + const viewSortId = createData?.createCoreViewSort?.id; + + jestExpectToBeDefined(viewSortId); + + const { data: destroyData } = await destroyOneCoreViewSort({ + input: { id: viewSortId }, + expectToFail: false, + }); + + expect(destroyData.destroyCoreViewSort).toBe(true); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-update.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-update.integration-spec.ts new file mode 100644 index 0000000000..a7b0ad45b0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/successful-view-sort-update.integration-spec.ts @@ -0,0 +1,156 @@ +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 { createOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/create-one-core-view-sort.util'; +import { destroyOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/destroy-one-core-view-sort.util'; +import { updateOneCoreViewSort } from 'test/integration/metadata/suites/view-sort/utils/update-one-core-view-sort.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { FieldMetadataType, ViewType } from 'twenty-shared/types'; + +import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; + +describe('View Sort update should succeed', () => { + let testObjectMetadataId: string; + let testFieldMetadataId: string; + let createdViewId: string; + let createdViewSortId: string; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'testViewSortUpdateObject', + namePlural: 'testViewSortUpdateObjects', + labelSingular: 'Test View Sort Update Object', + labelPlural: 'Test View Sort Update Objects', + icon: 'IconSort', + }, + }); + + testObjectMetadataId = objectMetadataId; + + const { + data: { + createOneField: { id: fieldMetadataId }, + }, + } = await createOneFieldMetadata({ + input: { + name: 'testSortUpdateField', + label: 'Test Sort Update Field', + type: FieldMetadataType.TEXT, + objectMetadataId: testObjectMetadataId, + isLabelSyncedWithName: true, + }, + gqlFields: 'id name label', + }); + + testFieldMetadataId = fieldMetadataId; + + const { data: viewData } = await createOneCoreView({ + expectToFail: false, + input: { + name: 'Test View For View Sort Update', + objectMetadataId: testObjectMetadataId, + type: ViewType.TABLE, + icon: 'IconBuildingSkyscraper', + }, + }); + + createdViewId = viewData?.createCoreView?.id; + jestExpectToBeDefined(createdViewId); + }); + + afterAll(async () => { + if (createdViewId) { + await destroyOneCoreView({ + expectToFail: false, + viewId: createdViewId, + }); + } + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: testObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + await deleteOneObjectMetadata({ + input: { idToDelete: testObjectMetadataId }, + }); + }); + + beforeEach(async () => { + const { data } = await createOneCoreViewSort({ + expectToFail: false, + input: { + viewId: createdViewId, + fieldMetadataId: testFieldMetadataId, + direction: ViewSortDirection.ASC, + }, + }); + + createdViewSortId = data?.createCoreViewSort?.id; + jestExpectToBeDefined(createdViewSortId); + }); + + afterEach(async () => { + if (createdViewSortId) { + await destroyOneCoreViewSort({ + expectToFail: false, + input: { id: createdViewSortId }, + }); + createdViewSortId = ''; + } + }); + + it('should update direction from ASC to DESC', async () => { + const { data } = await updateOneCoreViewSort({ + expectToFail: false, + input: { + id: createdViewSortId, + update: { + direction: ViewSortDirection.DESC, + }, + }, + }); + + expect(data.updateCoreViewSort).toMatchObject({ + id: createdViewSortId, + direction: ViewSortDirection.DESC, + }); + }); + + it('should update direction from DESC to ASC', async () => { + await updateOneCoreViewSort({ + expectToFail: false, + input: { + id: createdViewSortId, + update: { + direction: ViewSortDirection.DESC, + }, + }, + }); + + const { data } = await updateOneCoreViewSort({ + expectToFail: false, + input: { + id: createdViewSortId, + update: { + direction: ViewSortDirection.ASC, + }, + }, + }); + + expect(data.updateCoreViewSort).toMatchObject({ + id: createdViewSortId, + direction: ViewSortDirection.ASC, + }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/create-core-view-sort-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/create-core-view-sort-query-factory.util.ts new file mode 100644 index 0000000000..e44bbd1610 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/create-core-view-sort-query-factory.util.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import { VIEW_SORT_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input'; + +export const createCoreViewSortQueryFactory = ({ + gqlFields = VIEW_SORT_GQL_FIELDS, + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreateCoreViewSort($input: CreateViewSortInput!) { + createCoreViewSort(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/create-one-core-view-sort.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/create-one-core-view-sort.util.ts new file mode 100644 index 0000000000..7f2d9a27a4 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/create-one-core-view-sort.util.ts @@ -0,0 +1,40 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { createCoreViewSortQueryFactory } from 'test/integration/metadata/suites/view-sort/utils/create-core-view-sort-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input'; +import { type ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto'; + +export const createOneCoreViewSort = async ({ + input, + gqlFields, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createCoreViewSort: ViewSortDTO; +}> => { + const graphqlOperation = createCoreViewSortQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Sort creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Sort creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/delete-core-view-sort-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/delete-core-view-sort-query-factory.util.ts new file mode 100644 index 0000000000..ab91509ae4 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/delete-core-view-sort-query-factory.util.ts @@ -0,0 +1,17 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type DeleteViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input'; + +export const deleteCoreViewSortQueryFactory = ({ + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DeleteCoreViewSort($input: DeleteViewSortInput!) { + deleteCoreViewSort(input: $input) + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/delete-one-core-view-sort.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/delete-one-core-view-sort.util.ts new file mode 100644 index 0000000000..b3c818f442 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/delete-one-core-view-sort.util.ts @@ -0,0 +1,37 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { deleteCoreViewSortQueryFactory } from 'test/integration/metadata/suites/view-sort/utils/delete-core-view-sort-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type DeleteViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/delete-view-sort.input'; + +export const deleteOneCoreViewSort = async ({ + input, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + deleteCoreViewSort: boolean; +}> => { + const graphqlOperation = deleteCoreViewSortQueryFactory({ + input, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Sort deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Sort deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/destroy-core-view-sort-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/destroy-core-view-sort-query-factory.util.ts new file mode 100644 index 0000000000..dc22cc783b --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/destroy-core-view-sort-query-factory.util.ts @@ -0,0 +1,17 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type DestroyViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input'; + +export const destroyCoreViewSortQueryFactory = ({ + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DestroyCoreViewSort($input: DestroyViewSortInput!) { + destroyCoreViewSort(input: $input) + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/destroy-one-core-view-sort.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/destroy-one-core-view-sort.util.ts new file mode 100644 index 0000000000..13b25224ed --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/destroy-one-core-view-sort.util.ts @@ -0,0 +1,37 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { destroyCoreViewSortQueryFactory } from 'test/integration/metadata/suites/view-sort/utils/destroy-core-view-sort-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type DestroyViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/destroy-view-sort.input'; + +export const destroyOneCoreViewSort = async ({ + input, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + destroyCoreViewSort: boolean; +}> => { + const graphqlOperation = destroyCoreViewSortQueryFactory({ + input, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Sort destruction should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Sort destruction has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/find-core-view-sorts-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/find-core-view-sorts-query-factory.util.ts new file mode 100644 index 0000000000..59ed1cf19d --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/find-core-view-sorts-query-factory.util.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import { VIEW_SORT_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; + +export const findCoreViewSortsQueryFactory = ({ + gqlFields = VIEW_SORT_GQL_FIELDS, + viewId, +}: { + gqlFields?: string; + viewId: string; +}) => ({ + query: gql` + query GetCoreViewSorts($viewId: UUID!) { + getCoreViewSorts(viewId: $viewId) { + ${gqlFields} + } + } + `, + variables: { + viewId, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/find-core-view-sorts.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/find-core-view-sorts.util.ts new file mode 100644 index 0000000000..1e1e1e859d --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/find-core-view-sorts.util.ts @@ -0,0 +1,42 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { findCoreViewSortsQueryFactory } from 'test/integration/metadata/suites/view-sort/utils/find-core-view-sorts-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto'; + +export const findCoreViewSorts = async ({ + viewId, + gqlFields, + expectToFail, +}: { + viewId: string; + gqlFields?: string; + expectToFail?: boolean; +}): CommonResponseBody<{ + getCoreViewSorts: ViewSortDTO[]; +}> => { + const graphqlOperation = findCoreViewSortsQueryFactory({ + viewId, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Sort retrieval should have failed but did not', + }); + } + + if (!expectToFail) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Sort retrieval has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/update-core-view-sort-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/update-core-view-sort-query-factory.util.ts new file mode 100644 index 0000000000..a85233ea7d --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/update-core-view-sort-query-factory.util.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import { VIEW_SORT_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input'; + +export const updateCoreViewSortQueryFactory = ({ + gqlFields = VIEW_SORT_GQL_FIELDS, + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpdateCoreViewSort($input: UpdateViewSortInput!) { + updateCoreViewSort(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/update-one-core-view-sort.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/update-one-core-view-sort.util.ts new file mode 100644 index 0000000000..c658396b4e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-sort/utils/update-one-core-view-sort.util.ts @@ -0,0 +1,40 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { updateCoreViewSortQueryFactory } from 'test/integration/metadata/suites/view-sort/utils/update-core-view-sort-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input'; +import { type ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto'; + +export const updateOneCoreViewSort = async ({ + input, + gqlFields, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + updateCoreViewSort: ViewSortDTO; +}> => { + const graphqlOperation = updateCoreViewSortQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Sort update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Sort update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts b/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts index 9e0cafafcf..c1f247b5ab 100644 --- a/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts +++ b/packages/twenty-shared/src/metadata/constants/all-metadata-name.constant.ts @@ -5,6 +5,7 @@ export const ALL_METADATA_NAME = { viewField: 'viewField', viewFieldGroup: 'viewFieldGroup', viewGroup: 'viewGroup', + viewSort: 'viewSort', rowLevelPermissionPredicate: 'rowLevelPermissionPredicate', rowLevelPermissionPredicateGroup: 'rowLevelPermissionPredicateGroup', viewFilterGroup: 'viewFilterGroup',