diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useMoveViewColumns.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useMoveViewColumns.test.tsx
new file mode 100644
index 000000000..fe51ad100
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useMoveViewColumns.test.tsx
@@ -0,0 +1,47 @@
+import { renderHook } from '@testing-library/react';
+
+import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
+
+describe('useMoveViewColumns', () => {
+ it('should move columns to the left correctly', () => {
+ const { result } = renderHook(() => useMoveViewColumns());
+
+ const initialArray = [{ position: 0 }, { position: 1 }, { position: 2 }];
+
+ const movedArray = result.current.handleColumnMove('left', 1, initialArray);
+
+ expect(movedArray).toEqual([
+ { position: 0, index: 0 },
+ { position: 1, index: 1 },
+ { position: 2 },
+ ]);
+ });
+
+ it('should move columns to the right correctly', () => {
+ const { result } = renderHook(() => useMoveViewColumns());
+
+ const initialArray = [{ position: 0 }, { position: 1 }, { position: 2 }];
+
+ const movedArray = result.current.handleColumnMove(
+ 'right',
+ 1,
+ initialArray,
+ );
+
+ expect(movedArray).toEqual([
+ { position: 0 },
+ { position: 1, index: 1 },
+ { position: 2, index: 2 },
+ ]);
+ });
+
+ it('should handle invalid moves without modifying the array', () => {
+ const { result } = renderHook(() => useMoveViewColumns());
+
+ const initialArray = [{ position: 0 }, { position: 1 }, { position: 2 }];
+
+ const movedArray = result.current.handleColumnMove('left', 0, initialArray);
+
+ expect(movedArray).toEqual(initialArray);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar.test.tsx
new file mode 100644
index 000000000..54f0d60fc
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar.test.tsx
@@ -0,0 +1,322 @@
+import { act } from 'react-dom/test-utils';
+import { MemoryRouter, useSearchParams } from 'react-router-dom';
+import { MockedProvider } from '@apollo/client/testing';
+import { renderHook, waitFor } from '@testing-library/react';
+import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
+import { v4 as uuidv4 } from 'uuid';
+
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { generateDeleteOneRecordMutation } from '@/object-record/utils/generateDeleteOneRecordMutation';
+import { getScopedStateDeprecated } from '@/ui/utilities/recoil-scope/utils/getScopedStateDeprecated';
+import {
+ filterDefinition,
+ viewFilter,
+} from '@/views/hooks/__tests__/useViewBar_ViewFilters.test';
+import {
+ sortDefinition,
+ viewSort,
+} from '@/views/hooks/__tests__/useViewBar_ViewSorts.test';
+import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
+import { useViewBar } from '@/views/hooks/useViewBar';
+import { ViewScope } from '@/views/scopes/ViewScope';
+import { entityCountInCurrentViewScopedState } from '@/views/states/entityCountInCurrentViewScopedState';
+import { viewEditModeScopedState } from '@/views/states/viewEditModeScopedState';
+import { viewObjectMetadataIdScopeState } from '@/views/states/viewObjectMetadataIdScopeState';
+import { viewTypeScopedState } from '@/views/states/viewTypeScopedState';
+import { ViewType } from '@/views/types/ViewType';
+
+jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => {
+ return {
+ useMapFieldMetadataToGraphQLQuery: jest.fn().mockReturnValue(() => '\n'),
+ };
+});
+
+const mockedUuid = 'mocked-uuid';
+jest.mock('uuid');
+
+(uuidv4 as jest.Mock).mockReturnValue(mockedUuid);
+
+const mocks = [
+ {
+ request: {
+ query: generateDeleteOneRecordMutation({
+ objectMetadataItem: { nameSingular: 'view' } as ObjectMetadataItem,
+ }),
+ variables: { idToDelete: mockedUuid },
+ },
+ result: jest.fn(() => ({
+ data: { deleteView: { id: '' } },
+ })),
+ },
+];
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+
+ {children}
+
+
+
+);
+const renderHookConfig = {
+ wrapper: Wrapper,
+};
+
+const viewBarId = 'viewBarTestId';
+
+describe('useViewBar', () => {
+ it('should set and get current view Id', () => {
+ const { result } = renderHook(
+ () => useViewBar({ viewBarId }),
+ renderHookConfig,
+ );
+
+ expect(result.current.scopeId).toBe(viewBarId);
+ expect(result.current.currentViewId).toBeUndefined();
+
+ act(() => {
+ result.current.setCurrentViewId('testId');
+ });
+
+ expect(result.current.currentViewId).toBe('testId');
+ });
+
+ it('should create view and update url params', async () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+ const searchParams = useSearchParams();
+
+ return {
+ viewBar,
+ searchParams,
+ };
+ }, renderHookConfig);
+ await act(async () => {
+ await result.current.viewBar.createView('Test View');
+ });
+ expect(result.current.searchParams[0].get('view')).toBe(mockedUuid);
+ });
+
+ it('should delete current view and remove id from params', async () => {
+ const { result } = renderHook(
+ () => ({
+ viewBar: useViewBar({ viewBarId }),
+ searchParams: useSearchParams(),
+ }),
+ renderHookConfig,
+ );
+
+ await act(async () => {
+ await result.current.viewBar.createView('Test View');
+ result.current.viewBar.setCurrentViewId(mockedUuid);
+ });
+ expect(result.current.searchParams[0].get('view')).toBe(mockedUuid);
+
+ await act(async () => {
+ await result.current.viewBar.removeView(mockedUuid);
+ });
+ expect(result.current.searchParams[0].get('view')).toBeNull();
+
+ const addBookMutationMock = mocks[0].result;
+ await waitFor(() => expect(addBookMutationMock).toHaveBeenCalled());
+ });
+
+ it('should resetViewBar', async () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+ const {
+ currentViewFiltersState,
+ currentViewSortsState,
+ viewEditModeState,
+ } = useViewScopedStates({
+ viewScopeId: viewBarId,
+ });
+ const currentViewFilters = useRecoilValue(currentViewFiltersState);
+ const currentViewSorts = useRecoilValue(currentViewSortsState);
+ const viewEditMode = useRecoilValue(viewEditModeState);
+
+ return {
+ viewBar,
+ currentViewFilters,
+ currentViewSorts,
+ viewEditMode,
+ };
+ }, renderHookConfig);
+
+ act(() => {
+ result.current.viewBar.resetViewBar();
+ });
+
+ expect(result.current.currentViewFilters).toStrictEqual([]);
+ expect(result.current.currentViewSorts).toStrictEqual([]);
+ expect(result.current.viewEditMode).toBe('none');
+ });
+
+ it('should handleViewNameSubmit', async () => {
+ const { result } = renderHook(
+ () => useViewBar({ viewBarId }),
+ renderHookConfig,
+ );
+
+ await act(async () => {
+ await result.current.handleViewNameSubmit('New View Name');
+ });
+ });
+
+ it('should update edit mode', async () => {
+ const { result } = renderHook(
+ () => ({
+ viewBar: useViewBar({ viewBarId }),
+ editMode: useRecoilState(
+ getScopedStateDeprecated(viewEditModeScopedState, viewBarId),
+ )[0],
+ }),
+ renderHookConfig,
+ );
+
+ expect(result.current.editMode).toBe('none');
+ await act(async () => {
+ result.current.viewBar.setViewEditMode('create');
+ });
+
+ expect(result.current.editMode).toBe('create');
+ await act(async () => {
+ result.current.viewBar.setViewEditMode('edit');
+ });
+
+ expect(result.current.editMode).toBe('edit');
+ });
+
+ it('should update url param', async () => {
+ const { result } = renderHook(
+ () => ({
+ viewBar: useViewBar({ viewBarId }),
+ searchParams: useSearchParams(),
+ }),
+ renderHookConfig,
+ );
+ expect(result.current.searchParams[0].get('view')).toBeNull();
+ await act(async () => {
+ result.current.viewBar.changeViewInUrl('view1');
+ });
+ expect(result.current.searchParams[0].get('view')).toBe('view1');
+ });
+
+ it('should update object metadata id', async () => {
+ const { result } = renderHook(
+ () => ({
+ viewBar: useViewBar({ viewBarId }),
+ metadataId: useRecoilState(
+ getScopedStateDeprecated(viewObjectMetadataIdScopeState, viewBarId),
+ )[0],
+ }),
+ renderHookConfig,
+ );
+
+ expect(result.current.metadataId).toBeUndefined();
+ await act(async () => {
+ result.current.viewBar.setViewObjectMetadataId('newId');
+ });
+
+ expect(result.current.metadataId).toBe('newId');
+ });
+
+ it('should update view type', async () => {
+ const { result } = renderHook(
+ () => ({
+ viewBar: useViewBar({ viewBarId }),
+ ViewType: useRecoilState(
+ getScopedStateDeprecated(viewTypeScopedState, viewBarId),
+ )[0],
+ }),
+ renderHookConfig,
+ );
+
+ expect(result.current.ViewType).toBe('table');
+ await act(async () => {
+ result.current.viewBar.setViewType(ViewType.Kanban);
+ });
+
+ expect(result.current.ViewType).toBe('kanban');
+ });
+
+ it('should update count in current view', async () => {
+ const { result } = renderHook(
+ () => ({
+ viewBar: useViewBar({ viewBarId }),
+ count: useRecoilState(
+ getScopedStateDeprecated(
+ entityCountInCurrentViewScopedState,
+ viewBarId,
+ ),
+ )[0],
+ }),
+ renderHookConfig,
+ );
+
+ expect(result.current.count).toBe(0);
+ await act(async () => {
+ result.current.viewBar.setEntityCountInCurrentView(1);
+ });
+
+ expect(result.current.count).toBe(1);
+ });
+
+ it('should loadView', async () => {
+ const { result } = renderHook(
+ () => useViewBar({ viewBarId }),
+ renderHookConfig,
+ );
+
+ act(() => {
+ result.current.loadView(mockedUuid);
+ });
+ });
+
+ it('should updateCurrentView', async () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+ viewBar.setCurrentViewId(mockedUuid);
+
+ viewBar.setAvailableSortDefinitions([sortDefinition]);
+
+ viewBar.loadViewSorts(
+ {
+ edges: [
+ {
+ node: viewSort,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ mockedUuid,
+ );
+
+ viewBar.setAvailableFilterDefinitions([filterDefinition]);
+
+ viewBar.loadViewFilters(
+ {
+ edges: [
+ {
+ node: viewFilter,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ mockedUuid,
+ );
+
+ return { viewBar };
+ }, renderHookConfig);
+
+ await act(async () => {
+ await result.current.viewBar.updateCurrentView();
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewFields.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewFields.test.tsx
new file mode 100644
index 000000000..be182cb01
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewFields.test.tsx
@@ -0,0 +1,179 @@
+import { act } from 'react-dom/test-utils';
+import { MemoryRouter } from 'react-router-dom';
+import { gql } from '@apollo/client';
+import { MockedProvider } from '@apollo/client/testing';
+import { renderHook, waitFor } from '@testing-library/react';
+import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
+
+import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
+import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
+import { getScopedFamilyStateDeprecated } from '@/ui/utilities/recoil-scope/utils/getScopedFamilyStateDeprecated';
+import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
+import { useViewBar } from '@/views/hooks/useViewBar';
+import { ViewScope } from '@/views/scopes/ViewScope';
+import { currentViewFieldsScopedFamilyState } from '@/views/states/currentViewFieldsScopedFamilyState';
+import { ViewField } from '@/views/types/ViewField';
+
+jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => {
+ return {
+ useMapFieldMetadataToGraphQLQuery: jest.fn().mockReturnValue(() => '\n'),
+ };
+});
+
+const fieldMetadataId = '12ecdf87-506f-44a7-98c6-393e5f05b225';
+
+const fieldDefinition: ColumnDefinition = {
+ size: 1,
+ position: 1,
+ fieldMetadataId,
+ label: 'label',
+ iconName: 'icon',
+ type: 'TEXT',
+ metadata: {
+ placeHolder: 'placeHolder',
+ fieldName: 'fieldName',
+ },
+};
+const viewField: ViewField = {
+ id: '88930a16-685f-493b-a96b-91ca55666bba',
+ fieldMetadataId,
+ position: 1,
+ isVisible: true,
+ size: 1,
+ definition: fieldDefinition,
+};
+
+const viewBarId = 'viewBarTestId';
+
+const currentViewId = '23f5dceb-3482-4e3a-9bb4-2f52f2556be9';
+
+const mocks = [
+ {
+ request: {
+ query: gql`
+ mutation CreateOneViewField($input: ViewFieldCreateInput!) {
+ createViewField(data: $input) {
+ id
+ }
+ }
+ `,
+ variables: {
+ input: {
+ fieldMetadataId,
+ viewId: currentViewId,
+ isVisible: true,
+ size: 1,
+ position: 1,
+ },
+ },
+ },
+ result: jest.fn(() => ({
+ data: { createViewField: { id: '' } },
+ })),
+ },
+];
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+
+ {children}
+
+
+
+);
+
+const renderHookConfig = {
+ wrapper: Wrapper,
+};
+
+describe('useViewBar > viewFields', () => {
+ it('should update current fields', async () => {
+ const { result } = renderHook(
+ () => ({
+ viewBar: useViewBar({ viewBarId }),
+ currentFields: useRecoilState(
+ getScopedFamilyStateDeprecated(
+ currentViewFieldsScopedFamilyState,
+ viewBarId,
+ currentViewId,
+ ),
+ )[0],
+ }),
+ renderHookConfig,
+ );
+
+ expect(result.current.currentFields).toStrictEqual([]);
+ await act(async () => {
+ result.current.viewBar.setCurrentViewId(currentViewId);
+ result.current.viewBar.setViewObjectMetadataId('newId');
+ result.current.viewBar.persistViewFields([viewField]);
+ });
+
+ await waitFor(() =>
+ expect(result.current.currentFields).toEqual([viewField]),
+ );
+ });
+
+ it('should persist view fields', async () => {
+ const { result } = renderHook(
+ () => useViewBar({ viewBarId }),
+ renderHookConfig,
+ );
+
+ await act(async () => {
+ result.current.setCurrentViewId(currentViewId);
+ result.current.setViewObjectMetadataId('newId');
+ await result.current.persistViewFields([viewField]);
+ });
+
+ const persistViewFieldsMutation = mocks[0];
+
+ await waitFor(() =>
+ expect(persistViewFieldsMutation.result).toHaveBeenCalled(),
+ );
+ });
+
+ it('should load view fields', async () => {
+ const currentViewId = 'ac8807fd-0065-436d-bdf6-94333d75af6e';
+
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+
+ const { currentViewFieldsState } = useViewScopedStates({
+ viewScopeId: viewBarId,
+ });
+ const currentViewFields = useRecoilValue(currentViewFieldsState);
+
+ return {
+ viewBar,
+ currentViewFields,
+ };
+ }, renderHookConfig);
+
+ expect(result.current.currentViewFields).toStrictEqual([]);
+
+ await act(async () => {
+ result.current.viewBar.setAvailableFieldDefinitions([fieldDefinition]);
+
+ await result.current.viewBar.loadViewFields(
+ {
+ edges: [
+ {
+ node: viewField,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ currentViewId,
+ );
+ result.current.viewBar.setCurrentViewId(currentViewId);
+ });
+
+ expect(result.current.currentViewFields).toStrictEqual([viewField]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewFilters.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewFilters.test.tsx
new file mode 100644
index 000000000..6e6e0eb32
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewFilters.test.tsx
@@ -0,0 +1,211 @@
+import { act } from 'react-dom/test-utils';
+import { MemoryRouter } from 'react-router-dom';
+import { MockedProvider } from '@apollo/client/testing';
+import { renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
+import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
+import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
+import { useViewBar } from '@/views/hooks/useViewBar';
+import { ViewScope } from '@/views/scopes/ViewScope';
+import { ViewFilter } from '@/views/types/ViewFilter';
+import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+
+ {children}
+
+
+
+);
+const renderHookConfig = {
+ wrapper: Wrapper,
+};
+
+const viewBarId = 'viewBarTestId';
+
+export const filterDefinition: FilterDefinition = {
+ fieldMetadataId: '113ea8f8-1908-4c9c-9984-3f23c96b92f5',
+ label: 'label',
+ iconName: 'iconName',
+ type: 'TEXT',
+};
+
+export const viewFilter: ViewFilter = {
+ id: 'id',
+ fieldMetadataId: '113ea8f8-1908-4c9c-9984-3f23c96b92f5',
+ operand: ViewFilterOperand.Is,
+ value: 'value',
+ displayValue: 'displayValue',
+ definition: filterDefinition,
+};
+
+const currentViewId = '23f5dceb-3482-4e3a-9bb4-2f52f2556be9';
+
+describe('useViewBar > viewFilters', () => {
+ it('should load view filters', async () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+
+ const { currentViewFiltersState } = useViewScopedStates({
+ viewScopeId: viewBarId,
+ });
+ const currentViewFilters = useRecoilValue(currentViewFiltersState);
+
+ return {
+ viewBar,
+ currentViewFilters,
+ };
+ }, renderHookConfig);
+
+ expect(result.current.currentViewFilters).toStrictEqual([]);
+
+ await act(async () => {
+ result.current.viewBar.setAvailableFilterDefinitions([filterDefinition]);
+
+ await result.current.viewBar.loadViewFilters(
+ {
+ edges: [
+ {
+ node: viewFilter,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ currentViewId,
+ );
+ result.current.viewBar.setCurrentViewId(currentViewId);
+ });
+
+ expect(result.current.currentViewFilters).toStrictEqual([viewFilter]);
+ });
+
+ it('should upsertViewFilter', async () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+
+ viewBar.setAvailableFilterDefinitions([filterDefinition]);
+
+ viewBar.loadViewFilters(
+ {
+ edges: [
+ {
+ node: viewFilter,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ currentViewId,
+ );
+ viewBar.setCurrentViewId(currentViewId);
+
+ const { currentViewFiltersState } = useViewScopedStates({
+ viewScopeId: viewBarId,
+ });
+ const currentViewFilters = useRecoilValue(currentViewFiltersState);
+
+ return {
+ viewBar,
+ currentViewFilters,
+ };
+ }, renderHookConfig);
+
+ expect(result.current.currentViewFilters).toStrictEqual([viewFilter]);
+
+ const newFilters: Filter[] = [
+ {
+ fieldMetadataId: '113ea8f8-1908-4c9c-9984-3f23c96b92f5',
+ value: 'value',
+ displayValue: 'displayValue',
+ operand: ViewFilterOperand.IsNot,
+ definition: {
+ fieldMetadataId: 'id',
+ label: 'label',
+ iconName: 'icon',
+ type: 'TEXT',
+ },
+ },
+ {
+ fieldMetadataId: 'd9487757-183e-4fa0-a554-a980850cb23d',
+ value: 'value',
+ displayValue: 'displayValue',
+ operand: ViewFilterOperand.Contains,
+ definition: {
+ fieldMetadataId: 'id',
+ label: 'label',
+ iconName: 'icon',
+ type: 'TEXT',
+ },
+ },
+ ];
+
+ // upsert an existing filter
+ act(() => {
+ result.current.viewBar.upsertViewFilter(newFilters[0]);
+ });
+
+ expect(result.current.currentViewFilters).toStrictEqual([
+ { ...newFilters[0], id: viewFilter.id },
+ ]);
+
+ // upsert a new filter
+ act(() => {
+ result.current.viewBar.upsertViewFilter(newFilters[1]);
+ });
+
+ // expect currentViewFilters to contain both filters
+ expect(result.current.currentViewFilters).toStrictEqual([
+ { ...newFilters[0], id: viewFilter.id },
+ { ...newFilters[1], id: undefined },
+ ]);
+ });
+
+ it('should remove view filter', () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+
+ viewBar.setAvailableFilterDefinitions([filterDefinition]);
+
+ viewBar.loadViewFilters(
+ {
+ edges: [
+ {
+ node: viewFilter,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ currentViewId,
+ );
+ viewBar.setCurrentViewId(currentViewId);
+
+ const { currentViewFiltersState } = useViewScopedStates({
+ viewScopeId: viewBarId,
+ });
+ const currentViewFilters = useRecoilValue(currentViewFiltersState);
+
+ return {
+ viewBar,
+ currentViewFilters,
+ };
+ }, renderHookConfig);
+
+ expect(result.current.currentViewFilters).toStrictEqual([viewFilter]);
+
+ // remove an existing filter
+ act(() => {
+ result.current.viewBar.removeViewFilter(filterDefinition.fieldMetadataId);
+ });
+
+ expect(result.current.currentViewFilters).toStrictEqual([]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewSorts.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewSorts.test.tsx
new file mode 100644
index 000000000..e76422240
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useViewBar_ViewSorts.test.tsx
@@ -0,0 +1,198 @@
+import { act } from 'react-dom/test-utils';
+import { MemoryRouter } from 'react-router-dom';
+import { MockedProvider } from '@apollo/client/testing';
+import { renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
+import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
+import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
+import { useViewBar } from '@/views/hooks/useViewBar';
+import { ViewScope } from '@/views/scopes/ViewScope';
+import { ViewSort } from '@/views/types/ViewSort';
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+
+ {children}
+
+
+
+);
+const renderHookConfig = {
+ wrapper: Wrapper,
+};
+
+const viewBarId = 'viewBarTestId';
+
+export const sortDefinition: SortDefinition = {
+ fieldMetadataId: '12ecdf87-506f-44a7-98c6-393e5f05b225',
+ label: 'label',
+ iconName: 'icon',
+};
+
+export const viewSort: ViewSort = {
+ id: '88930a16-685f-493b-a96b-91ca55666bba',
+ fieldMetadataId: '12ecdf87-506f-44a7-98c6-393e5f05b225',
+ direction: 'asc',
+ definition: sortDefinition,
+};
+
+describe('View Sorts', () => {
+ const currentViewId = 'ac8807fd-0065-436d-bdf6-94333d75af6e';
+
+ it('should load view sorts', async () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+
+ const { currentViewSortsState } = useViewScopedStates({
+ viewScopeId: viewBarId,
+ });
+ const currentViewSorts = useRecoilValue(currentViewSortsState);
+
+ return {
+ viewBar,
+ currentViewSorts,
+ };
+ }, renderHookConfig);
+
+ expect(result.current.currentViewSorts).toStrictEqual([]);
+
+ await act(async () => {
+ result.current.viewBar.setAvailableSortDefinitions([sortDefinition]);
+
+ await result.current.viewBar.loadViewSorts(
+ {
+ edges: [
+ {
+ node: viewSort,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ currentViewId,
+ );
+ result.current.viewBar.setCurrentViewId(currentViewId);
+ });
+
+ expect(result.current.currentViewSorts).toStrictEqual([viewSort]);
+ });
+
+ it('should upsertViewSort', async () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+
+ viewBar.setAvailableSortDefinitions([sortDefinition]);
+
+ viewBar.loadViewSorts(
+ {
+ edges: [
+ {
+ node: viewSort,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ currentViewId,
+ );
+ viewBar.setCurrentViewId(currentViewId);
+
+ const { currentViewSortsState } = useViewScopedStates({
+ viewScopeId: viewBarId,
+ });
+ const currentViewSorts = useRecoilValue(currentViewSortsState);
+
+ return {
+ viewBar,
+ currentViewSorts,
+ };
+ }, renderHookConfig);
+
+ expect(result.current.currentViewSorts).toStrictEqual([viewSort]);
+
+ const newSortFieldMetadataId = 'd9487757-183e-4fa0-a554-a980850cb23d';
+
+ const newSorts: Sort[] = [
+ {
+ fieldMetadataId: viewSort.fieldMetadataId,
+ direction: 'desc',
+ definition: sortDefinition,
+ },
+ {
+ fieldMetadataId: newSortFieldMetadataId,
+ direction: 'asc',
+ definition: {
+ ...sortDefinition,
+ fieldMetadataId: newSortFieldMetadataId,
+ },
+ },
+ ];
+
+ // upsert an existing sort
+ act(() => {
+ result.current.viewBar.upsertViewSort(newSorts[0]);
+ });
+
+ expect(result.current.currentViewSorts).toStrictEqual([
+ { ...newSorts[0], id: viewSort.id },
+ ]);
+
+ // upsert a new sort
+ act(() => {
+ result.current.viewBar.upsertViewSort(newSorts[1]);
+ });
+
+ // expect currentViewSorts to contain both sorts
+ expect(result.current.currentViewSorts).toStrictEqual([
+ { ...newSorts[0], id: viewSort.id },
+ { ...newSorts[1], id: undefined },
+ ]);
+ });
+
+ it('should remove view sort', () => {
+ const { result } = renderHook(() => {
+ const viewBar = useViewBar({ viewBarId });
+
+ viewBar.setAvailableSortDefinitions([sortDefinition]);
+
+ viewBar.loadViewSorts(
+ {
+ edges: [
+ {
+ node: viewSort,
+ cursor: '',
+ },
+ ],
+ pageInfo: { hasNextPage: false, startCursor: '', endCursor: '' },
+ },
+ currentViewId,
+ );
+ viewBar.setCurrentViewId(currentViewId);
+
+ const { currentViewSortsState } = useViewScopedStates({
+ viewScopeId: viewBarId,
+ });
+ const currentViewSorts = useRecoilValue(currentViewSortsState);
+
+ return {
+ viewBar,
+ currentViewSorts,
+ };
+ }, renderHookConfig);
+
+ expect(result.current.currentViewSorts).toStrictEqual([viewSort]);
+
+ // remove an existing sort
+ act(() => {
+ result.current.viewBar.removeViewSort(sortDefinition.fieldMetadataId);
+ });
+
+ expect(result.current.currentViewSorts).toStrictEqual([]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts
new file mode 100644
index 000000000..38c7b7c51
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts
@@ -0,0 +1,314 @@
+import { FieldMetadata } from '@/object-record/field/types/FieldMetadata';
+import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
+import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';
+import { BoardFieldDefinition } from '@/object-record/record-board/types/BoardFieldDefinition';
+import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
+import { ViewField } from '@/views/types/ViewField';
+import { ViewFilter } from '@/views/types/ViewFilter';
+import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
+import { ViewSort } from '@/views/types/ViewSort';
+import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
+import { mapViewFieldsToBoardFieldDefinitions } from '@/views/utils/mapViewFieldsToBoardFieldDefinitions';
+import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
+import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
+import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
+
+const baseDefinition = {
+ fieldMetadataId: 'fieldMetadataId',
+ label: 'label',
+ iconName: 'iconName',
+};
+
+describe('mapViewSortsToSorts', () => {
+ it('should map each ViewSort object to a corresponding Sort object', () => {
+ const viewSorts: ViewSort[] = [
+ {
+ id: 'id',
+ fieldMetadataId: 'fieldMetadataId',
+ direction: 'asc',
+ definition: baseDefinition,
+ },
+ ];
+ const expectedSorts: Sort[] = [
+ {
+ fieldMetadataId: 'fieldMetadataId',
+ direction: 'asc',
+ definition: baseDefinition,
+ },
+ ];
+ expect(mapViewSortsToSorts(viewSorts)).toEqual(expectedSorts);
+ });
+});
+
+describe('mapViewFiltersToFilters', () => {
+ it('should map each ViewFilter object to a corresponding Filter object', () => {
+ const viewFilters: ViewFilter[] = [
+ {
+ id: 'id',
+ fieldMetadataId: '1',
+ value: 'testValue',
+ displayValue: 'Test Display Value',
+ operand: ViewFilterOperand.Is,
+ definition: {
+ ...baseDefinition,
+ type: 'FULL_NAME',
+ },
+ },
+ ];
+ const expectedFilters: Filter[] = [
+ {
+ fieldMetadataId: '1',
+ value: 'testValue',
+ displayValue: 'Test Display Value',
+ operand: ViewFilterOperand.Is,
+ definition: {
+ ...baseDefinition,
+ type: 'FULL_NAME',
+ },
+ },
+ ];
+ expect(mapViewFiltersToFilters(viewFilters)).toEqual(expectedFilters);
+ });
+});
+
+describe('mapViewFieldsToColumnDefinitions', () => {
+ it('should map visible ViewFields to ColumnDefinitions and filter out missing fieldMetadata', () => {
+ const viewFields: ViewField[] = [
+ {
+ id: '1',
+ fieldMetadataId: '1',
+ position: 1,
+ size: 1,
+ isVisible: false,
+ definition: {
+ fieldMetadataId: '1',
+ label: 'label 1',
+ metadata: { fieldName: 'fieldName 1' },
+ infoTooltipContent: 'infoTooltipContent 1',
+ iconName: 'iconName 1',
+ type: 'TEXT',
+ position: 1,
+ size: 1,
+ isVisible: false,
+ viewFieldId: '1',
+ },
+ },
+ {
+ id: '2',
+ fieldMetadataId: '2',
+ position: 2,
+ size: 2,
+ isVisible: false,
+ definition: {
+ fieldMetadataId: '2',
+ label: 'label 2',
+ metadata: { fieldName: 'fieldName 2' },
+ infoTooltipContent: 'infoTooltipContent 2',
+ iconName: 'iconName 2',
+ type: 'TEXT',
+ position: 2,
+ size: 1,
+ isVisible: false,
+ viewFieldId: '2',
+ },
+ },
+ {
+ id: '3',
+ fieldMetadataId: '3',
+ position: 3,
+ size: 3,
+ isVisible: true,
+ definition: {
+ fieldMetadataId: '3',
+ label: 'label 3',
+ metadata: { fieldName: 'fieldName 3' },
+ infoTooltipContent: 'infoTooltipContent 3',
+ iconName: 'iconName 3',
+ type: 'TEXT',
+ position: 3,
+ size: 1,
+ isVisible: false,
+ viewFieldId: '3',
+ },
+ },
+ ];
+
+ const fieldsMetadata: ColumnDefinition[] = [
+ {
+ fieldMetadataId: '1',
+ label: 'label 1',
+ position: 1,
+ metadata: { fieldName: 'fieldName 1' },
+ infoTooltipContent: 'infoTooltipContent 1',
+ iconName: 'iconName 1',
+ type: 'TEXT',
+ size: 1,
+ },
+ {
+ fieldMetadataId: '3',
+ label: 'label 3',
+ position: 3,
+ metadata: { fieldName: 'fieldName 3' },
+ infoTooltipContent: 'infoTooltipContent 3',
+ iconName: 'iconName 3',
+ type: 'TEXT',
+ size: 3,
+ },
+ ];
+
+ const expectedColumnDefinitions: ColumnDefinition[] = [
+ {
+ fieldMetadataId: '1',
+ label: 'label 1',
+ metadata: { fieldName: 'fieldName 1' },
+ infoTooltipContent: 'infoTooltipContent 1',
+ iconName: 'iconName 1',
+ type: 'TEXT',
+ size: 1,
+ position: 1,
+ isVisible: false,
+ viewFieldId: '1',
+ },
+ {
+ fieldMetadataId: '3',
+ label: 'label 3',
+ metadata: { fieldName: 'fieldName 3' },
+ infoTooltipContent: 'infoTooltipContent 3',
+ iconName: 'iconName 3',
+ type: 'TEXT',
+ size: 3,
+ position: 3,
+ isVisible: true,
+ viewFieldId: '3',
+ },
+ ];
+
+ const actualColumnDefinitions = mapViewFieldsToColumnDefinitions(
+ viewFields,
+ fieldsMetadata,
+ );
+
+ expect(actualColumnDefinitions).toEqual(expectedColumnDefinitions);
+ });
+});
+
+describe('mapViewFieldsToBoardFieldDefinitions', () => {
+ it('should map visible ViewFields to BoardFieldDefinitions and filter out missing fieldMetadata', () => {
+ const viewFields = [
+ {
+ id: 1,
+ fieldMetadataId: 1,
+ position: 1,
+ isVisible: true,
+ },
+ {
+ id: 2,
+ fieldMetadataId: 2,
+ position: 2,
+ isVisible: false,
+ },
+ {
+ id: 3,
+ fieldMetadataId: 3,
+ position: 3,
+ isVisible: true,
+ },
+ ];
+
+ const fieldsMetadata = [
+ {
+ fieldMetadataId: 1,
+ label: 'Field 1',
+ metadata: {},
+ position: 1,
+ infoTooltipContent: 'Tooltip content for Field 1',
+ iconName: 'icon-field-1',
+ type: 'string',
+ },
+ {
+ fieldMetadataId: 3,
+ label: 'Field 3',
+ metadata: {},
+ position: 3,
+ infoTooltipContent: 'Tooltip for Field 3',
+ iconName: 'icon-field-3',
+ type: 'number',
+ },
+ ];
+
+ const expectedBoardFieldDefinitions = [
+ {
+ fieldMetadataId: 1,
+ label: 'Field 1',
+ metadata: {},
+ position: 1,
+ infoTooltipContent: 'Tooltip content for Field 1',
+ iconName: 'icon-field-1',
+ type: 'string',
+ isVisible: true,
+ viewFieldId: 1,
+ },
+ {
+ fieldMetadataId: 3,
+ label: 'Field 3',
+ metadata: {},
+ position: 3,
+ infoTooltipContent: 'Tooltip for Field 3',
+ iconName: 'icon-field-3',
+ type: 'number',
+ isVisible: true,
+ viewFieldId: 3,
+ },
+ ];
+
+ const actualBoardFieldDefinitions = mapViewFieldsToBoardFieldDefinitions(
+ viewFields as unknown as ViewField[],
+ fieldsMetadata as unknown as BoardFieldDefinition[],
+ );
+
+ expect(actualBoardFieldDefinitions).toEqual(expectedBoardFieldDefinitions);
+ });
+});
+
+describe('mapColumnDefinitionsToViewFields', () => {
+ it('should map ColumnDefinitions to ViewFields, setting defaults and using viewFieldId if present', () => {
+ const columnDefinitions = [
+ {
+ fieldMetadataId: 1,
+ position: 1,
+ isVisible: true,
+ viewFieldId: 'custom-id-1',
+ },
+ {
+ fieldMetadataId: 2,
+ position: 2,
+ size: 200,
+ isVisible: false,
+ },
+ ];
+
+ const expectedViewFields = [
+ {
+ id: 'custom-id-1',
+ fieldMetadataId: 1,
+ position: 1,
+ isVisible: true,
+ definition: columnDefinitions[0],
+ },
+ {
+ id: '',
+ fieldMetadataId: 2,
+ position: 2,
+ size: 200,
+ isVisible: false,
+ definition: columnDefinitions[1],
+ },
+ ];
+
+ const actualViewFields = mapColumnDefinitionsToViewFields(
+ columnDefinitions as unknown as ColumnDefinition[],
+ );
+
+ expect(actualViewFields).toEqual(expectedViewFields);
+ });
+});