Improve record group aggregate query performance (#15828)

This PR is a first step for improving the performance on boards and
table with groups.

It is related to :
https://github.com/twentyhq/core-team-issues/issues/1870

Here we implement only a groupBy query for aggregate values in the group
section.

This also allows to improve the DX of aggregate computing and group by
query creation and parsing.

## Demo 

Main : 



https://github.com/user-attachments/assets/5d2a8077-5322-4928-a551-f03583bcfb87



This PR : 



https://github.com/user-attachments/assets/d0e82b28-72c3-40f0-b5cb-045f1a736ffb



## Aggregate update bug fix

This PR also solves a bug with aggregate update that was already present
on main.

The bug is linked to core views not being updated properly during a
modification of the aggregate operation on a view.

We should probably improve the view lifecycle and state management
because it is a bit too complex right now.

Main : 


https://github.com/user-attachments/assets/10dbfb8b-dfa0-4f21-8698-d222871a43e7

This PR : 


https://github.com/user-attachments/assets/bac41890-5191-4e4c-b82b-19b1039e9ab5

## Miscellaneous 

- Fixed optimistic rendering of group by queries, when adding a new
record, the aggregate recomputes well.

## TODO 

- We might want to improve the optimistic for group by queries that
don't have records nor more than one dimension.
This commit is contained in:
Lucas Bordeau
2025-11-23 20:40:23 +01:00
committed by GitHub
parent 4d55fef874
commit 061cc897af
61 changed files with 1356 additions and 930 deletions
@@ -1,14 +1,19 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { usePersistView } from '@/views/hooks/internal/usePersistView';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { useRecoilCallback } from 'recoil';
import {
isDefined,
upsertIntoArrayOfObjectsComparingId,
} from 'twenty-shared/utils';
import { type CoreView } from '~/generated/graphql';
export const useUpdateViewAggregate = () => {
const { canPersistChanges } = useCanPersistViewChanges();
@@ -16,62 +21,64 @@ export const useUpdateViewAggregate = () => {
contextStoreCurrentViewIdComponentState,
);
const { updateView } = usePersistView();
const { loadRecordIndexStates } = useLoadRecordIndexStates();
const setRecordIndexKanbanAggregateOperationState = useSetRecoilState(
recordIndexKanbanAggregateOperationState,
);
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
const updateViewAggregate = useCallback(
({
kanbanAggregateOperationFieldMetadataId,
kanbanAggregateOperation,
objectMetadataId,
}: {
kanbanAggregateOperationFieldMetadataId: string | null;
kanbanAggregateOperation: ExtendedAggregateOperations | null;
objectMetadataId: string;
}) => {
if (!canPersistChanges) {
return;
}
const convertedKanbanAggregateOperation = isDefined(
const updateViewAggregate = useRecoilCallback(
({ set }) =>
async ({
kanbanAggregateOperationFieldMetadataId,
kanbanAggregateOperation,
)
? convertExtendedAggregateOperationToAggregateOperation(
kanbanAggregateOperation,
)
: null;
objectMetadataItem,
}: {
kanbanAggregateOperationFieldMetadataId: string | null;
kanbanAggregateOperation: ExtendedAggregateOperations | null;
objectMetadataItem: ObjectMetadataItem;
}) => {
if (!canPersistChanges) {
return;
}
if (!isDefined(currentViewId)) {
return;
}
const convertedKanbanAggregateOperation = isDefined(
kanbanAggregateOperation,
)
? convertExtendedAggregateOperationToAggregateOperation(
kanbanAggregateOperation,
)
: null;
updateView({
id: currentViewId,
input: {
kanbanAggregateOperationFieldMetadataId,
kanbanAggregateOperation: convertedKanbanAggregateOperation,
},
});
if (!isDefined(currentViewId)) {
return;
}
setRecordIndexKanbanAggregateOperationState({
operation: kanbanAggregateOperation,
fieldMetadataId: kanbanAggregateOperationFieldMetadataId,
});
const updatedViewResult = await updateView({
id: currentViewId,
input: {
kanbanAggregateOperationFieldMetadataId,
kanbanAggregateOperation: convertedKanbanAggregateOperation,
},
});
refreshCoreViewsByObjectMetadataId(objectMetadataId);
},
[
canPersistChanges,
currentViewId,
updateView,
setRecordIndexKanbanAggregateOperationState,
refreshCoreViewsByObjectMetadataId,
],
if (updatedViewResult.status === 'successful') {
const updatedCoreView = updatedViewResult.response.data
?.updateCoreView as CoreView;
if (!isDefined(updatedCoreView)) {
return;
}
set(coreViewsState, (currentCoreViews) =>
upsertIntoArrayOfObjectsComparingId(
currentCoreViews,
updatedCoreView,
),
);
const updatedView = convertCoreViewToView(updatedCoreView);
loadRecordIndexStates(updatedView, objectMetadataItem);
}
},
[canPersistChanges, currentViewId, updateView, loadRecordIndexStates],
);
return {