Refacto views (#10272)

In this huge (sorry!) PR:
- introducing objectMetadataItem in contextStore instead of
objectMetadataId which is more convenient
- splitting some big hooks into smaller parts to avoid re-renders
- removing Effects to avoid re-renders (especially onViewChange)
- making the view prefetch separate from favorites to avoid re-renders
- making the view prefetch load a state and add selectors on top of it
to avoir re-renders

As a result, the performance is WAY better (I suspect the favorite
implementation to trigger a lot of re-renders unfortunately).
However, we are still facing a random app freeze on view creation. I
could not investigate the root cause. As this seems to be already there
in the precedent release, we can move forward but this seems a urgent
follow up to me ==> EDIT: I've found the root cause after a few ours of
deep dive... an infinite loop in RecordTableNoRecordGroupBodyEffect...

prastoin edit: close https://github.com/twentyhq/twenty/issues/10253

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
Charles Bochet
2025-02-18 13:51:07 +01:00
committed by GitHub
parent 103dff4bd0
commit fb42046033
125 changed files with 1607 additions and 1582 deletions

View File

@ -4,41 +4,54 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
import { AppPath } from '@/types/AppPath';
import { ViewType } from '@/views/types/ViewType';
import { getCompanyObjectMetadataItem } from '~/testing/mock-data/companies';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { mockedUserData } from '~/testing/mock-data/users';
jest.mock('@/prefetch/hooks/usePrefetchedData');
const setupMockPrefetchedData = (viewId?: string) => {
const companyObjectMetadata = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'company',
);
jest.mocked(usePrefetchedData).mockReturnValue({
isDataPrefetched: true,
records: viewId
? [
{
id: viewId,
__typename: 'object',
objectMetadataId: companyObjectMetadata?.id,
},
]
: [],
});
};
const renderHooks = (withCurrentUser: boolean) => {
const renderHooks = ({
withCurrentUser,
withExistingView,
}: {
withCurrentUser: boolean;
withExistingView: boolean;
}) => {
const { result } = renderHook(
() => {
const setCurrentUser = useSetRecoilState(currentUserState);
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
const setPrefetchViews = useSetRecoilState(prefetchViewsState);
setObjectMetadataItems(generatedMockObjectMetadataItems);
if (withExistingView) {
setPrefetchViews([
{
id: 'viewId',
name: 'Test View',
objectMetadataId: getCompanyObjectMetadataItem().id,
type: ViewType.Table,
key: null,
isCompact: false,
viewFields: [],
viewGroups: [],
viewSorts: [],
kanbanFieldMetadataId: '',
kanbanAggregateOperation: AGGREGATE_OPERATIONS.count,
icon: '',
kanbanAggregateOperationFieldMetadataId: '',
position: 0,
viewFilters: [],
__typename: 'View',
},
]);
}
if (withCurrentUser) {
setCurrentUser(mockedUserData);
}
@ -52,23 +65,31 @@ const renderHooks = (withCurrentUser: boolean) => {
};
describe('useDefaultHomePagePath', () => {
it('should return proper path when no currentUser', () => {
setupMockPrefetchedData();
const { result } = renderHooks(false);
const { result } = renderHooks({
withCurrentUser: false,
withExistingView: false,
});
expect(result.current.defaultHomePagePath).toEqual(AppPath.SignInUp);
});
it('should return proper path when no currentUser and existing view', () => {
setupMockPrefetchedData('viewId');
const { result } = renderHooks(false);
const { result } = renderHooks({
withCurrentUser: false,
withExistingView: true,
});
expect(result.current.defaultHomePagePath).toEqual(AppPath.SignInUp);
});
it('should return proper path when currentUser is defined', () => {
setupMockPrefetchedData();
const { result } = renderHooks(true);
const { result } = renderHooks({
withCurrentUser: true,
withExistingView: false,
});
expect(result.current.defaultHomePagePath).toEqual('/objects/companies');
});
it('should return proper path when currentUser is defined and view exists', () => {
setupMockPrefetchedData('viewId');
const { result } = renderHooks(true);
const { result } = renderHooks({
withCurrentUser: true,
withExistingView: true,
});
expect(result.current.defaultHomePagePath).toEqual(
'/objects/companies?viewId=viewId',
);

View File

@ -2,10 +2,8 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState';
import { ObjectPathInfo } from '@/navigation/types/ObjectPathInfo';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
import { AppPath } from '@/types/AppPath';
import { View } from '@/views/types/View';
import { useCallback, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
@ -15,7 +13,7 @@ export const useDefaultHomePagePath = () => {
const currentUser = useRecoilValue(currentUserState);
const { activeObjectMetadataItems, alphaSortedActiveObjectMetadataItems } =
useFilteredObjectMetadataItems();
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const prefetchViews = useRecoilValue(prefetchViewsState);
const lastVisitedObjectMetadataItemId = useRecoilValue(
lastVisitedObjectMetadataItemIdState,
);
@ -31,8 +29,10 @@ export const useDefaultHomePagePath = () => {
const getFirstView = useCallback(
(objectMetadataItemId: string | undefined | null) =>
views.find((view) => view.objectMetadataId === objectMetadataItemId),
[views],
prefetchViews.find(
(view) => view.objectMetadataId === objectMetadataItemId,
),
[prefetchViews],
);
const firstObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {

View File

@ -1,16 +1,11 @@
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useLazyPrefetchedData } from '@/prefetch/hooks/useLazyPrefetchData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
import { View } from '@/views/types/View';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared';
export const useSetLastVisitedViewForObjectMetadataNamePlural = () => {
const { records: views, findManyRecords } = useLazyPrefetchedData<View>(
PrefetchKey.AllViews,
);
const setLastVisitedViewForObjectMetadataNamePlural = useRecoilCallback(
({ set, snapshot }) =>
async ({
@ -20,7 +15,7 @@ export const useSetLastVisitedViewForObjectMetadataNamePlural = () => {
objectNamePlural: string;
viewId: string;
}) => {
await findManyRecords();
const views = snapshot.getLoadable(prefetchViewsState).getValue();
const view = views.find((view: View) => view.id === viewId);
@ -54,7 +49,7 @@ export const useSetLastVisitedViewForObjectMetadataNamePlural = () => {
});
}
},
[findManyRecords, views],
[],
);
return {