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

@ -1,9 +1,9 @@
import { useRecoilCallback } from 'recoil';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
import { isDefined } from 'twenty-shared';
@ -24,11 +24,10 @@ export const useDeleteCombinedViewFilterGroup = (
);
const currentViewIdCallbackState = useRecoilComponentCallbackStateV2(
currentViewIdComponentState,
viewBarComponentId,
contextStoreCurrentViewIdComponentState,
);
const { getViewFromCache } = useGetViewFromCache();
const { getViewFromPrefetchState } = useGetViewFromPrefetchState();
const deleteCombinedViewFilterGroup = useRecoilCallback(
({ snapshot, set }) =>
@ -56,7 +55,7 @@ export const useDeleteCombinedViewFilterGroup = (
return;
}
const currentView = await getViewFromCache(currentViewId);
const currentView = await getViewFromPrefetchState(currentViewId);
if (!currentView) {
return;
@ -99,7 +98,7 @@ export const useDeleteCombinedViewFilterGroup = (
},
[
currentViewIdCallbackState,
getViewFromCache,
getViewFromPrefetchState,
unsavedToDeleteViewFilterGroupIdsCallbackState,
unsavedToUpsertViewFilterGroupsCallbackState,
],

View File

@ -116,6 +116,7 @@ export const useCreateOneRecord = <
objectMetadataItem,
objectMetadataItems,
record: recordCreatedInCache,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
});

View File

@ -17,6 +17,7 @@ import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
@ -79,6 +80,7 @@ const meta: Meta<typeof MultipleFiltersDropdownButton> = {
</RecordIndexContextProvider>
);
},
ContextStoreDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
ComponentDecorator,

View File

@ -36,7 +36,6 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
const {
viewType,
currentContentId,
recordIndexId,
objectMetadataItem,
onContentChange,
resetContent,
@ -64,9 +63,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
const {
handleRecordGroupFieldChange: setRecordGroupField,
resetRecordGroupField,
} = useHandleRecordGroupField({
viewBarComponentId: recordIndexId,
});
} = useHandleRecordGroupField();
const newSelectFieldSettingsUrl = getSettingsPath(
SettingsPath.ObjectNewFieldConfigure,

View File

@ -1,7 +1,6 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent';
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
@ -16,6 +15,7 @@ import { ViewType } from '@/views/types/ViewType';
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@ -45,21 +45,18 @@ const meta: Meta<typeof ObjectOptionsDropdownContent> = {
value={{ instanceId, onColumnsChange: () => {} }}
>
<ViewComponentInstanceContext.Provider value={{ instanceId }}>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId }}
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<MemoryRouter
initialEntries={['/one', '/two', { pathname: '/three' }]}
initialIndex={1}
>
<Story />
</MemoryRouter>
</ContextStoreComponentInstanceContext.Provider>
<Story />
</MemoryRouter>
</ViewComponentInstanceContext.Provider>
</RecordTableComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
);
},
ContextStoreDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
ComponentDecorator,

View File

@ -26,13 +26,12 @@ type useObjectOptionsForBoardParams = {
export const useObjectOptionsForBoard = ({
objectNameSingular,
recordBoardId,
viewBarId,
}: useObjectOptionsForBoardParams) => {
const [recordIndexFieldDefinitions, setRecordIndexFieldDefinitions] =
useRecoilState(recordIndexFieldDefinitionsState);
const { saveViewFields } = useSaveCurrentViewFields(viewBarId);
const { updateCurrentView } = useUpdateCurrentView(viewBarId);
const { saveViewFields } = useSaveCurrentViewFields();
const { updateCurrentView } = useUpdateCurrentView();
const [isCompactModeActive, setIsCompactModeActive] =
useRecoilComponentStateV2(

View File

@ -23,7 +23,7 @@ export const useRecordGroupReorder = ({
viewBarId,
viewType,
}: UseRecordGroupHandlersParams) => {
const setRecordGroup = useSetRecordGroup(viewBarId);
const setRecordGroup = useSetRecordGroup();
const visibleRecordGroupIdsFamilySelector = useRecoilComponentCallbackStateV2(
visibleRecordGroupIdsComponentFamilySelector,
@ -78,7 +78,7 @@ export const useRecordGroupReorder = ({
];
}, []);
setRecordGroup(updatedRecordGroups);
setRecordGroup(updatedRecordGroups, viewBarId);
saveViewGroups(
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups),
);
@ -86,6 +86,7 @@ export const useRecordGroupReorder = ({
[
saveViewGroups,
setRecordGroup,
viewBarId,
viewType,
visibleRecordGroupIdsFamilySelector,
],

View File

@ -1,32 +1,33 @@
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetRecordGroup = (viewId?: string) => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2(
recordGroupIdsComponentState,
viewId,
);
const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2(
recordGroupFieldMetadataComponentState,
viewId,
);
export const useSetRecordGroup = () => {
return useRecoilCallback(
({ snapshot, set }) =>
(recordGroups: RecordGroupDefinition[]) => {
(recordGroups: RecordGroupDefinition[], recordIndexId: string) => {
const objectMetadataItem = snapshot
.getLoadable(
contextStoreCurrentObjectMetadataItemComponentState.atomFamily({
instanceId: 'main-context-store',
}),
)
.getValue();
if (!objectMetadataItem) {
return;
}
const currentRecordGroupIds = getSnapshotValue(
snapshot,
recordIndexRecordGroupIdsState,
recordGroupIdsComponentState.atomFamily({
instanceId: recordIndexId,
}),
);
const fieldMetadataId = recordGroups?.[0]?.fieldMetadataId;
const fieldMetadata = fieldMetadataId
@ -36,12 +37,19 @@ export const useSetRecordGroup = (viewId?: string) => {
: undefined;
const currentFieldMetadata = getSnapshotValue(
snapshot,
recordGroupFieldMetadataState,
recordGroupFieldMetadataComponentState.atomFamily({
instanceId: recordIndexId,
}),
);
// Set the field metadata linked to the record groups
if (!isDeeplyEqual(fieldMetadata, currentFieldMetadata)) {
set(recordGroupFieldMetadataState, fieldMetadata);
set(
recordGroupFieldMetadataComponentState.atomFamily({
instanceId: recordIndexId,
}),
fieldMetadata,
);
}
// Set the record groups by id
@ -75,12 +83,13 @@ export const useSetRecordGroup = (viewId?: string) => {
}
// Set the record group ids
set(recordIndexRecordGroupIdsState, recordGroupIds);
set(
recordGroupIdsComponentState.atomFamily({
instanceId: recordIndexId,
}),
recordGroupIds,
);
},
[
objectMetadataItem.fields,
recordGroupFieldMetadataState,
recordIndexRecordGroupIdsState,
],
[],
);
};

View File

@ -1,52 +1,26 @@
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { useRecoilState } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
import { ContextStoreCurrentViewTypeEffect } from '@/context-store/components/ContextStoreCurrentViewTypeEffect';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewField } from '@/views/types/ViewField';
import { ViewGroup } from '@/views/types/ViewGroup';
import { ViewType } from '@/views/types/ViewType';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useCallback } from 'react';
import { FeatureFlagKey } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const StyledContainer = styled.div`
display: flex;
@ -64,9 +38,7 @@ const StyledContainerWithPadding = styled.div`
`;
export const RecordIndexContainer = () => {
const [recordIndexViewType, setRecordIndexViewType] = useRecoilState(
recordIndexViewTypeState,
);
const [recordIndexViewType] = useRecoilState(recordIndexViewTypeState);
const {
objectNamePlural,
@ -75,128 +47,12 @@ export const RecordIndexContainer = () => {
objectNameSingular,
} = useRecordIndexContextOrThrow();
const setRecordGroup = useSetRecordGroup(recordIndexId);
const { columnDefinitions, sortDefinitions } =
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const setRecordIndexViewFilterGroups = useSetRecoilState(
recordIndexViewFilterGroupsState,
);
const setRecordIndexFilters = useSetRecoilState(recordIndexFiltersState);
const setRecordIndexSorts = useSetRecoilState(recordIndexSortsState);
const setRecordIndexIsCompactModeActive = useSetRecoilState(
recordIndexIsCompactModeActiveState,
);
const setRecordIndexViewKanbanFieldMetadataIdState = useSetRecoilState(
recordIndexKanbanFieldMetadataIdState,
);
const setRecordIndexViewKanbanAggregateOperationState = useSetRecoilState(
recordIndexKanbanAggregateOperationState,
);
const {
setTableViewFilterGroups,
setTableFilters,
setTableSorts,
setTableColumns,
} = useRecordTable({
recordTableId: recordIndexId,
});
const onViewFieldsChange = useRecoilCallback(
({ set, snapshot }) =>
(viewFields: ViewField[]) => {
const newFieldDefinitions = mapViewFieldsToColumnDefinitions({
viewFields,
columnDefinitions,
});
setTableColumns(newFieldDefinitions);
const existingRecordIndexFieldDefinitions = snapshot
.getLoadable(recordIndexFieldDefinitionsState)
.getValue();
if (
!isDeeplyEqual(
existingRecordIndexFieldDefinitions,
newFieldDefinitions,
)
) {
set(recordIndexFieldDefinitionsState, newFieldDefinitions);
}
for (const viewField of viewFields) {
const viewFieldMetadataType = objectMetadataItem.fields?.find(
(field) => field.id === viewField.fieldMetadataId,
)?.type;
const aggregateOperationForViewField = snapshot
.getLoadable(
viewFieldAggregateOperationState({
viewFieldId: viewField.id,
}),
)
.getValue();
const convertedViewFieldAggregateOperation = isDefined(
viewField.aggregateOperation,
)
? convertAggregateOperationToExtendedAggregateOperation(
viewField.aggregateOperation,
viewFieldMetadataType,
)
: viewField.aggregateOperation;
if (
aggregateOperationForViewField !==
convertedViewFieldAggregateOperation
) {
set(
viewFieldAggregateOperationState({
viewFieldId: viewField.id,
}),
convertedViewFieldAggregateOperation,
);
}
}
},
[columnDefinitions, objectMetadataItem.fields, setTableColumns],
);
const onViewGroupsChange = useCallback(
(viewGroups: ViewGroup[]) => {
const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({
objectMetadataItem,
viewGroups,
});
setRecordGroup(newGroupDefinitions);
},
[objectMetadataItem, setRecordGroup],
);
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
contextStoreTargetedRecordsRuleComponentState,
);
const { filterableFieldMetadataItems } = useFilterableFieldMetadataItems(
objectMetadataItem.id,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
return (
<>
<ContextStoreCurrentViewTypeEffect
viewType={
recordIndexViewType === ViewType.Table
? ContextStoreViewType.Table
: ContextStoreViewType.Kanban
}
/>
<StyledContainer>
<InformationBannerWrapper />
<RecordFieldValueSelectorContextProvider>
@ -210,60 +66,6 @@ export const RecordIndexContainer = () => {
viewType={recordIndexViewType ?? ViewType.Table}
/>
}
onCurrentViewChange={(view) => {
if (!view) {
return;
}
onViewFieldsChange(view.viewFields);
onViewGroupsChange(view.viewGroups);
setTableViewFilterGroups(view.viewFilterGroups ?? []);
setTableFilters(
mapViewFiltersToFilters(
view.viewFilters,
filterableFieldMetadataItems,
),
);
setRecordIndexFilters(
mapViewFiltersToFilters(
view.viewFilters,
filterableFieldMetadataItems,
),
);
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
setContextStoreTargetedRecordsRule((prev) => ({
...prev,
filters: mapViewFiltersToFilters(
view.viewFilters,
filterableFieldMetadataItems,
),
}));
setTableSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexViewType(view.type);
setRecordIndexViewKanbanFieldMetadataIdState(
view.kanbanFieldMetadataId,
);
const kanbanAggregateOperationFieldMetadataType =
objectMetadataItem.fields?.find(
(field) =>
field.id === view.kanbanAggregateOperationFieldMetadataId,
)?.type;
setRecordIndexViewKanbanAggregateOperationState({
operation: isDefined(view.kanbanAggregateOperation)
? convertAggregateOperationToExtendedAggregateOperation(
view.kanbanAggregateOperation,
kanbanAggregateOperationFieldMetadataType,
)
: view.kanbanAggregateOperation,
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
});
setRecordIndexIsCompactModeActive(view.isCompact);
}}
/>
<RecordIndexViewBarEffect
objectNamePlural={objectNamePlural}

View File

@ -1,31 +0,0 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect } from 'react';
export const RecordIndexContainerContextStoreObjectMetadataEffect = () => {
const setContextStoreCurrentObjectMetadataItem = useSetRecoilComponentStateV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
const { objectNamePlural } = useRecordIndexContextOrThrow();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
useEffect(() => {
setContextStoreCurrentObjectMetadataItem(objectMetadataItem.id);
return () => {
setContextStoreCurrentObjectMetadataItem(null);
};
}, [objectMetadataItem.id, setContextStoreCurrentObjectMetadataItem]);
return null;
};

View File

@ -0,0 +1,97 @@
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect';
import { RecordIndexLoadBaseOnContextStoreEffect } from '@/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect';
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { capitalize } from 'twenty-shared';
const StyledIndexContainer = styled.div`
display: flex;
height: 100%;
width: 100%;
`;
export const RecordIndexContainerGater = () => {
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
const contextStoreCurrentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
mainContextStoreComponentInstanceId,
);
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const recordIndexId = `${objectMetadataItem.namePlural}-${contextStoreCurrentViewId}`;
const handleIndexRecordsLoaded = useRecoilCallback(
({ set }) =>
() => {
// TODO: find a better way to reset this state ?
set(lastShowPageRecordIdState, null);
},
[],
);
const { indexIdentifierUrl } = useHandleIndexIdentifierClick({
objectMetadataItem,
recordIndexId,
});
return (
<>
<RecordIndexContextProvider
value={{
recordIndexId,
objectNamePlural: objectMetadataItem.namePlural,
objectNameSingular: objectMetadataItem.nameSingular,
objectMetadataItem,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
indexIdentifierUrl,
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: getActionMenuIdFromRecordIndexId(recordIndexId),
}}
>
<PageTitle
title={`${capitalize(objectMetadataItem.namePlural)}`}
/>
<RecordIndexPageHeader />
<PageBody>
<StyledIndexContainer>
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</PageBody>
</ActionMenuComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
<RecordIndexLoadBaseOnContextStoreEffect />
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</>
);
};

View File

@ -0,0 +1,52 @@
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
export const RecordIndexLoadBaseOnContextStoreEffect = () => {
const { loadRecordIndexStates } = useLoadRecordIndexStates();
const contextStoreCurrentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
);
const [loadedViewId, setLoadedViewId] = useState<string | undefined>(
undefined,
);
const view = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
viewId: contextStoreCurrentViewId ?? '',
}),
);
const objectMetadataItem = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemComponentState,
);
useEffect(() => {
if (loadedViewId === contextStoreCurrentViewId) {
return;
}
if (!isDefined(objectMetadataItem)) {
return;
}
if (isDefined(view)) {
loadRecordIndexStates(view, objectMetadataItem);
setLoadedViewId(contextStoreCurrentViewId);
}
}, [
contextStoreCurrentViewId,
loadRecordIndexStates,
loadedViewId,
objectMetadataItem,
view,
]);
return <></>;
};

View File

@ -1,7 +1,7 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AppPath } from '@/types/AppPath';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { getAppPath } from '~/utils/navigation/getAppPath';
export const useHandleIndexIdentifierClick = ({
@ -12,7 +12,7 @@ export const useHandleIndexIdentifierClick = ({
objectMetadataItem: ObjectMetadataItem;
}) => {
const currentViewId = useRecoilComponentValueV2(
currentViewIdComponentState,
contextStoreCurrentViewIdComponentState,
recordIndexId,
);

View File

@ -1,29 +1,22 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
import { ViewGroup } from '@/views/types/ViewGroup';
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type UseHandleRecordGroupFieldParams = {
viewBarComponentId: string;
};
export const useHandleRecordGroupField = ({
viewBarComponentId,
}: UseHandleRecordGroupFieldParams) => {
export const useHandleRecordGroupField = () => {
const { createViewGroupRecords, deleteViewGroupRecords } =
usePersistViewGroupRecords();
const currentViewIdCallbackState = useRecoilComponentCallbackStateV2(
currentViewIdComponentState,
viewBarComponentId,
contextStoreCurrentViewIdComponentState,
);
const { getViewFromCache } = useGetViewFromCache();
const { getViewFromPrefetchState } = useGetViewFromPrefetchState();
const handleRecordGroupFieldChange = useRecoilCallback(
({ snapshot }) =>
@ -36,7 +29,7 @@ export const useHandleRecordGroupField = ({
return;
}
const view = await getViewFromCache(currentViewId);
const view = await getViewFromPrefetchState(currentViewId);
if (isUndefinedOrNull(view)) {
return;
@ -105,7 +98,7 @@ export const useHandleRecordGroupField = ({
createViewGroupRecords,
deleteViewGroupRecords,
currentViewIdCallbackState,
getViewFromCache,
getViewFromPrefetchState,
],
);
@ -120,7 +113,7 @@ export const useHandleRecordGroupField = ({
return;
}
const view = await getViewFromCache(currentViewId);
const view = await getViewFromPrefetchState(currentViewId);
if (isUndefinedOrNull(view)) {
return;
@ -132,7 +125,11 @@ export const useHandleRecordGroupField = ({
await deleteViewGroupRecords(view.viewGroups);
},
[deleteViewGroupRecords, currentViewIdCallbackState, getViewFromCache],
[
deleteViewGroupRecords,
currentViewIdCallbackState,
getViewFromPrefetchState,
],
);
return { handleRecordGroupFieldChange, resetRecordGroupField };

View File

@ -6,13 +6,14 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared';
type UseLoadRecordIndexBoardProps = {
@ -41,14 +42,16 @@ export const useLoadRecordIndexBoardColumn = ({
const recordIndexViewFilterGroups = useRecoilValue(
recordIndexViewFilterGroupsState,
);
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
const currentRecordFilters = useRecoilComponentValueV2(
currentRecordFiltersComponentState,
);
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
const { filterValueDependencies } = useFilterValueDependencies();
const requestFilters = computeViewRecordGqlOperationFilter(
filterValueDependencies,
recordIndexFilters,
currentRecordFilters,
objectMetadataItem?.fields ?? [],
recordIndexViewFilterGroups,
);

View File

@ -0,0 +1,281 @@
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { View } from '@/views/types/View';
import { ViewField } from '@/views/types/ViewField';
import { ViewGroup } from '@/views/types/ViewGroup';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useCallback } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useLoadRecordIndexStates = () => {
const setContextStoreTargetedRecordsRuleComponentState =
useSetRecoilComponentStateV2(contextStoreTargetedRecordsRuleComponentState);
const setRecordIndexViewFilterGroups = useSetRecoilState(
recordIndexViewFilterGroupsState,
);
const setRecordIndexFilters = useSetRecoilState(recordIndexFiltersState);
const setRecordIndexSorts = useSetRecoilState(recordIndexSortsState);
const setRecordIndexIsCompactModeActive = useSetRecoilState(
recordIndexIsCompactModeActiveState,
);
const setRecordIndexViewType = useSetRecoilState(recordIndexViewTypeState);
const setRecordIndexViewKanbanFieldMetadataIdState = useSetRecoilState(
recordIndexKanbanFieldMetadataIdState,
);
const setRecordIndexViewKanbanAggregateOperationState = useSetRecoilState(
recordIndexKanbanAggregateOperationState,
);
const setRecordGroup = useSetRecordGroup();
const { setTableColumns } = useSetTableColumns();
const onViewFieldsChange = useRecoilCallback(
({ set, snapshot }) =>
(
viewFields: ViewField[],
objectMetadataItem: ObjectMetadataItem,
recordIndexId: string,
) => {
const activeFieldMetadataItems = objectMetadataItem.fields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
);
const filterableFieldMetadataItems = snapshot
.getLoadable(
availableFieldMetadataItemsForFilterFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
)
.getValue();
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
fields: activeFieldMetadataItems,
});
const columnDefinitions: ColumnDefinition<FieldMetadata>[] =
activeFieldMetadataItems
.map((field, index) =>
formatFieldMetadataItemAsColumnDefinition({
position: index,
field,
objectMetadataItem,
}),
)
.filter(filterAvailableTableColumns)
.map((column) => {
const existsInFilterDefinitions =
filterableFieldMetadataItems.some(
(fieldMetadataItem) =>
fieldMetadataItem.id === column.fieldMetadataId,
);
const existsInSortDefinitions = sortDefinitions.some(
(sort) => sort.fieldMetadataId === column.fieldMetadataId,
);
return {
...column,
isFilterable: existsInFilterDefinitions,
isSortable: existsInSortDefinitions,
};
});
const newFieldDefinitions = mapViewFieldsToColumnDefinitions({
viewFields,
columnDefinitions,
});
setTableColumns(newFieldDefinitions, recordIndexId);
const existingRecordIndexFieldDefinitions = snapshot
.getLoadable(recordIndexFieldDefinitionsState)
.getValue();
if (
!isDeeplyEqual(
existingRecordIndexFieldDefinitions,
newFieldDefinitions,
)
) {
set(recordIndexFieldDefinitionsState, newFieldDefinitions);
}
for (const viewField of viewFields) {
const viewFieldMetadataType = objectMetadataItem.fields?.find(
(field) => field.id === viewField.fieldMetadataId,
)?.type;
const aggregateOperationForViewField = snapshot
.getLoadable(
viewFieldAggregateOperationState({
viewFieldId: viewField.id,
}),
)
.getValue();
const convertedViewFieldAggregateOperation = isDefined(
viewField.aggregateOperation,
)
? convertAggregateOperationToExtendedAggregateOperation(
viewField.aggregateOperation,
viewFieldMetadataType,
)
: viewField.aggregateOperation;
if (
aggregateOperationForViewField !==
convertedViewFieldAggregateOperation
) {
set(
viewFieldAggregateOperationState({
viewFieldId: viewField.id,
}),
convertedViewFieldAggregateOperation,
);
}
}
},
[setTableColumns],
);
const onViewGroupsChange = useCallback(
(
viewGroups: ViewGroup[],
objectMetadataItem: ObjectMetadataItem,
recordIndexId: string,
) => {
const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({
objectMetadataItem,
viewGroups,
});
setRecordGroup(newGroupDefinitions, recordIndexId);
},
[setRecordGroup],
);
const loadRecordIndexStates = useRecoilCallback(
({ snapshot, set }) =>
async (view: View, objectMetadataItem: ObjectMetadataItem) => {
const recordIndexId = `${objectMetadataItem.namePlural}-${view.id}`;
const filterableFieldMetadataItems = snapshot
.getLoadable(
availableFieldMetadataItemsForFilterFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
)
.getValue();
onViewFieldsChange(view.viewFields, objectMetadataItem, recordIndexId);
onViewGroupsChange(view.viewGroups, objectMetadataItem, recordIndexId);
set(
tableViewFilterGroupsComponentState.atomFamily({
instanceId: recordIndexId,
}),
view.viewFilterGroups ?? [],
);
set(
tableFiltersComponentState.atomFamily({
instanceId: recordIndexId,
}),
mapViewFiltersToFilters(
view.viewFilters,
filterableFieldMetadataItems,
),
);
setRecordIndexFilters(
mapViewFiltersToFilters(
view.viewFilters,
filterableFieldMetadataItems,
),
);
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
setContextStoreTargetedRecordsRuleComponentState((prev) => ({
...prev,
filters: mapViewFiltersToFilters(
view.viewFilters,
filterableFieldMetadataItems,
),
}));
const activeFieldMetadataItems = objectMetadataItem.fields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
);
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
fields: activeFieldMetadataItems,
});
set(
tableSortsComponentState.atomFamily({
instanceId: recordIndexId,
}),
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexViewType(view.type);
setRecordIndexViewKanbanFieldMetadataIdState(
view.kanbanFieldMetadataId,
);
const kanbanAggregateOperationFieldMetadataType =
objectMetadataItem.fields?.find(
(field) =>
field.id === view.kanbanAggregateOperationFieldMetadataId,
)?.type;
setRecordIndexViewKanbanAggregateOperationState({
operation: isDefined(view.kanbanAggregateOperation)
? convertAggregateOperationToExtendedAggregateOperation(
view.kanbanAggregateOperation,
kanbanAggregateOperationFieldMetadataType,
)
: view.kanbanAggregateOperation,
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
});
setRecordIndexIsCompactModeActive(view.isCompact);
},
[
onViewFieldsChange,
onViewGroupsChange,
setContextStoreTargetedRecordsRuleComponentState,
setRecordIndexFilters,
setRecordIndexIsCompactModeActive,
setRecordIndexSorts,
setRecordIndexViewFilterGroups,
setRecordIndexViewKanbanAggregateOperationState,
setRecordIndexViewKanbanFieldMetadataIdState,
setRecordIndexViewType,
],
);
return {
loadRecordIndexStates,
};
};

View File

@ -3,9 +3,6 @@ import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
import { ContextStoreCurrentViewTypeEffect } from '@/context-store/components/ContextStoreCurrentViewTypeEffect';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { RecordShowContainerContextStoreObjectMetadataIdEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreObjectMetadataIdEffect';
import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreTargetedRecordsEffect';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
@ -45,16 +42,9 @@ export const RecordShowContainer = ({
return (
<>
<RecordShowContainerContextStoreObjectMetadataIdEffect
recordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
<RecordShowContainerContextStoreTargetedRecordsEffect
recordId={objectRecordId}
/>
<ContextStoreCurrentViewTypeEffect
viewType={ContextStoreViewType.ShowPage}
/>
{recordFromStore && recordFromStore.deletedAt && (
<InformationBannerDeletedRecord
recordId={objectRecordId}

View File

@ -1,30 +0,0 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect } from 'react';
export const RecordShowContainerContextStoreObjectMetadataIdEffect = ({
recordId,
objectNameSingular,
}: {
recordId: string;
objectNameSingular: string;
}) => {
const setContextStoreCurrentObjectMetadataId = useSetRecoilComponentStateV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: objectNameSingular,
});
useEffect(() => {
setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id);
return () => {
setContextStoreCurrentObjectMetadataId(null);
};
}, [recordId, setContextStoreCurrentObjectMetadataId, objectMetadataItem.id]);
return null;
};

View File

@ -39,7 +39,6 @@ export const useRecordShowPagePagination = (
const { filter, orderBy } =
useQueryVariablesFromActiveFieldsOfViewOrDefaultView({
objectMetadataItem,
viewId: viewIdQueryParam,
});
const { loading: loadingCursor, pageInfo: currentRecordsPageInfo } =

View File

@ -23,14 +23,12 @@ import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecord
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
import { AppPath } from '@/types/AppPath';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { View } from '@/views/types/View';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { useLingui } from '@lingui/react/macro';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
@ -123,12 +121,10 @@ export const RecordDetailRelationSection = ({
scopeId: dropdownId,
});
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const indexView = views.find(
(view) =>
view.key === 'INDEX' &&
view.objectMetadataId === relationObjectMetadataItem.id,
const indexViewId = useRecoilValue(
prefetchIndexViewIdFromObjectMetadataItemFamilySelector({
objectMetadataItemId: relationObjectMetadataItem.id,
}),
);
const filterQueryParams = {
@ -139,7 +135,7 @@ export const RecordDetailRelationSection = ({
},
},
},
view: indexView?.id,
view: indexViewId,
};
const filterLinkHref = getAppPath(

View File

@ -75,7 +75,7 @@ export const RecordTableWithWrappers = ({
ActionBarHotkeyScope.ActionBar,
);
const { saveViewFields } = useSaveCurrentViewFields(viewBarId);
const { saveViewFields } = useSaveCurrentViewFields();
const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular });

View File

@ -22,11 +22,9 @@ import { onColumnsChangeComponentState } from '@/object-record/record-table/stat
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -53,11 +51,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
recordTableId,
);
const tableColumnsState = useRecoilComponentCallbackStateV2(
tableColumnsComponentState,
recordTableId,
);
const setAvailableTableColumns = useRecoilCallback(
({ snapshot, set }) =>
(columns: ColumnDefinition<FieldMetadata>[]) => {
@ -74,29 +67,11 @@ export const useRecordTable = (props?: useRecordTableProps) => {
[availableTableColumnsState],
);
const setTableColumns = useRecoilCallback(
({ snapshot, set }) =>
(columns: ColumnDefinition<FieldMetadata>[]) => {
const tableColumns = getSnapshotValue(snapshot, tableColumnsState);
if (isDeeplyEqual(tableColumns, columns)) {
return;
}
set(tableColumnsState, columns);
},
[tableColumnsState],
);
const setOnEntityCountChange = useSetRecoilComponentStateV2(
onEntityCountChangeComponentState,
recordTableId,
);
const setTableViewFilterGroups = useSetRecoilComponentStateV2(
tableViewFilterGroupsComponentState,
recordTableId,
);
const setTableFilters = useSetRecoilComponentStateV2(
tableFiltersComponentState,
recordTableId,
@ -255,12 +230,10 @@ export const useRecordTable = (props?: useRecordTableProps) => {
return {
onColumnsChange,
setAvailableTableColumns,
setTableViewFilterGroups,
setTableFilters,
setTableSorts,
setOnEntityCountChange,
setRecordTableData,
setTableColumns,
leaveTableFocus,
setRowSelected,
resetTableRowSelection,

View File

@ -0,0 +1,33 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useRecoilCallback } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetTableColumns = () => {
const setTableColumns = useRecoilCallback(
({ snapshot, set }) =>
(columns: ColumnDefinition<FieldMetadata>[], recordTableId: string) => {
const tableColumns = getSnapshotValue(
snapshot,
tableColumnsComponentState.atomFamily({
instanceId: recordTableId,
}),
);
if (isDeeplyEqual(tableColumns, columns)) {
return;
}
set(
tableColumnsComponentState.atomFamily({
instanceId: recordTableId,
}),
columns,
);
},
[],
);
return { setTableColumns };
};

View File

@ -4,9 +4,12 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns';
import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns';
import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ColumnDefinition } from '../types/ColumnDefinition';
@ -15,10 +18,12 @@ type useRecordTableProps = {
};
export const useTableColumns = (props?: useRecordTableProps) => {
const { onColumnsChange, setTableColumns } = useRecordTable({
const { onColumnsChange } = useRecordTable({
recordTableId: props?.recordTableId,
});
const { setTableColumns } = useSetTableColumns();
const availableTableColumns = useRecoilComponentValueV2(
availableTableColumnsComponentState,
props?.recordTableId,
@ -35,13 +40,18 @@ export const useTableColumns = (props?: useRecordTableProps) => {
const { handleColumnMove } = useMoveViewColumns();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordTableComponentInstanceContext,
props?.recordTableId,
);
const handleColumnsChange = useCallback(
async (columns: ColumnDefinition<FieldMetadata>[]) => {
setTableColumns(columns);
setTableColumns(columns, instanceId);
await onColumnsChange?.(columns);
},
[onColumnsChange, setTableColumns],
[setTableColumns, instanceId, onColumnsChange],
);
const handleColumnVisibilityChange = useCallback(

View File

@ -55,6 +55,8 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
lastShowPageRecordIdState,
);
const [hasInitialized, setHasInitialized] = useState(false);
const { scrollToPosition } = useScrollToPosition();
useEffect(() => {
@ -141,8 +143,11 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
return;
}
findManyRecords();
}, [currentWorkspaceMember, findManyRecords]);
if (!hasInitialized) {
findManyRecords();
setHasInitialized(true);
}
}, [currentWorkspaceMember, findManyRecords, hasInitialized]);
return <></>;
};