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); + }); +});