Refactor Views by cleaning the code, relying on apolloCache and improving performances (#4516)

* Wip refactoring view

* Post merge conflicts

* Fix review

* Add create view capability

* Fix create object missing view

* Fix tests
This commit is contained in:
Charles Bochet
2024-03-20 14:21:58 +01:00
committed by GitHub
parent 20e14cb455
commit cfb0cce9b8
392 changed files with 3474 additions and 4410 deletions

View File

@ -1,27 +1,11 @@
import { act } from 'react-dom/test-utils';
import { MemoryRouter, useSearchParams } from 'react-router-dom';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook, waitFor } from '@testing-library/react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { RecoilRoot } 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';
const mockedUuid = 'mocked-uuid';
jest.mock('uuid');
@ -49,225 +33,19 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
>
<MockedProvider mocks={mocks} addTypename={false}>
<RecoilRoot>
<ViewScope viewScopeId="viewScopeId">{children}</ViewScope>
<ViewScope viewScopeId="viewScopeId" onCurrentViewChange={() => {}}>
{children}
</ViewScope>
</RecoilRoot>
</MockedProvider>
</MemoryRouter>
);
const renderHookConfig = {
const _renderHookConfig = {
wrapper: Wrapper,
};
const viewBarId = 'viewBarTestId';
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 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([viewSort], mockedUuid);
viewBar.setAvailableFilterDefinitions([filterDefinition]);
viewBar.loadViewFilters([viewFilter], mockedUuid);
return { viewBar };
}, renderHookConfig);
await act(async () => {
await result.current.viewBar.updateCurrentView();
});
});
it('should set and get current view Id', () => {});
});

View File

@ -1,170 +0,0 @@
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/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';
const fieldMetadataId = '12ecdf87-506f-44a7-98c6-393e5f05b225';
const fieldDefinition: ColumnDefinition<FieldMetadata> = {
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) {
__typename
position
isVisible
fieldMetadataId
viewId
id
size
createdAt
updatedAt
}
}
`,
variables: {
input: {
fieldMetadataId,
viewId: currentViewId,
isVisible: true,
size: 1,
position: 1,
},
},
},
result: jest.fn(() => ({
data: { createViewField: { id: '' } },
})),
},
];
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<MockedProvider mocks={mocks} addTypename={false}>
<RecoilRoot>
<ViewScope viewScopeId="viewScopeId">{children}</ViewScope>
</RecoilRoot>
</MockedProvider>
</MemoryRouter>
);
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([viewField], currentViewId);
result.current.viewBar.setCurrentViewId(currentViewId);
});
expect(result.current.currentViewFields).toStrictEqual([viewField]);
});
});

View File

@ -1,178 +0,0 @@
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 }) => (
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<MockedProvider addTypename={false}>
<RecoilRoot>
<ViewScope viewScopeId="viewScopeId">{children}</ViewScope>
</RecoilRoot>
</MockedProvider>
</MemoryRouter>
);
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([viewFilter], 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([viewFilter], 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([viewFilter], 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([]);
});
});

View File

@ -1,165 +0,0 @@
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 }) => (
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<MockedProvider addTypename={false}>
<RecoilRoot>
<ViewScope viewScopeId="viewScopeId">{children}</ViewScope>
</RecoilRoot>
</MockedProvider>
</MemoryRouter>
);
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([viewSort], 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([viewSort], 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([viewSort], 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([]);
});
});