Skip to content

Commit

Permalink
[Aggregate queries for table views - #2] Add aggregate queries footer…
Browse files Browse the repository at this point in the history
… for simple views (#9025)

In this PR, we are introducing aggregate queries on table views, behind
a feature flag.
This does not work with view groups yet, nor with views that have
records until the bottom. (both will be tackled next)
  • Loading branch information
ijreilly authored Dec 12, 2024
1 parent 5f2a39d commit 05cd0d1
Show file tree
Hide file tree
Showing 20 changed files with 662 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { buildRecordGqlFieldsAggregate } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate';
import { buildRecordGqlFieldsAggregateForRecordBoard } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForRecordBoard';
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
Expand Down Expand Up @@ -42,7 +42,7 @@ export const useAggregateRecordsForRecordBoardColumn = () => {
);
}

const recordGqlFieldsAggregate = buildRecordGqlFieldsAggregate({
const recordGqlFieldsAggregate = buildRecordGqlFieldsAggregateForRecordBoard({
objectMetadataItem: objectMetadataItem,
recordIndexKanbanAggregateOperation: recordIndexKanbanAggregateOperation,
kanbanFieldName: kanbanFieldName,
Expand Down Expand Up @@ -74,12 +74,13 @@ export const useAggregateRecordsForRecordBoardColumn = () => {
skip: !isAggregateQueryEnabled,
});

const { value, label } = computeAggregateValueAndLabel(
const { value, label } = computeAggregateValueAndLabel({
data,
objectMetadataItem,
recordIndexKanbanAggregateOperation,
kanbanFieldName,
);
fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId,
aggregateOperation: recordIndexKanbanAggregateOperation?.operation,
fallbackFieldName: kanbanFieldName,
});

return {
aggregateValue: isAggregateQueryEnabled ? value : recordCount,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { buildRecordGqlFieldsAggregate } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate';
import { buildRecordGqlFieldsAggregateForRecordBoard } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForRecordBoard';
import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { FieldMetadataType } from '~/generated-metadata/graphql';

const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
const MOCK_KANBAN_FIELD = 'stage';

describe('buildRecordGqlFieldsAggregate', () => {
describe('buildRecordGqlFieldsAggregateForRecordBoard', () => {
const mockObjectMetadata: ObjectMetadataItem = {
id: '123',
nameSingular: 'opportunity',
Expand Down Expand Up @@ -50,7 +50,7 @@ describe('buildRecordGqlFieldsAggregate', () => {
operation: AGGREGATE_OPERATIONS.sum,
};

const result = buildRecordGqlFieldsAggregate({
const result = buildRecordGqlFieldsAggregateForRecordBoard({
objectMetadataItem: mockObjectMetadata,
recordIndexKanbanAggregateOperation: kanbanAggregateOperation,
kanbanFieldName: MOCK_KANBAN_FIELD,
Expand All @@ -67,7 +67,7 @@ describe('buildRecordGqlFieldsAggregate', () => {
operation: AGGREGATE_OPERATIONS.count,
};

const result = buildRecordGqlFieldsAggregate({
const result = buildRecordGqlFieldsAggregateForRecordBoard({
objectMetadataItem: mockObjectMetadata,
recordIndexKanbanAggregateOperation: operation,
kanbanFieldName: MOCK_KANBAN_FIELD,
Expand All @@ -85,7 +85,7 @@ describe('buildRecordGqlFieldsAggregate', () => {
};

expect(() =>
buildRecordGqlFieldsAggregate({
buildRecordGqlFieldsAggregateForRecordBoard({
objectMetadataItem: mockObjectMetadata,
recordIndexKanbanAggregateOperation: operation,
kanbanFieldName: MOCK_KANBAN_FIELD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/Agg
import { FieldMetadataType } from '~/generated/graphql';

const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
const MOCK_KANBAN_FIELD = 'stage';
const MOCK_KANBAN_FIELD_NAME = 'stage';

describe('computeAggregateValueAndLabel', () => {
const mockObjectMetadata: ObjectMetadataItem = {
Expand All @@ -20,12 +20,13 @@ describe('computeAggregateValueAndLabel', () => {
} as ObjectMetadataItem;

it('should return empty object for empty data', () => {
const result = computeAggregateValueAndLabel(
{},
mockObjectMetadata,
{ fieldMetadataId: MOCK_FIELD_ID, operation: AGGREGATE_OPERATIONS.sum },
MOCK_KANBAN_FIELD,
);
const result = computeAggregateValueAndLabel({
data: {},
objectMetadataItem: mockObjectMetadata,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
});

expect(result).toEqual({});
});
Expand All @@ -37,12 +38,13 @@ describe('computeAggregateValueAndLabel', () => {
},
};

const result = computeAggregateValueAndLabel(
mockData,
mockObjectMetadata,
{ fieldMetadataId: MOCK_FIELD_ID, operation: AGGREGATE_OPERATIONS.sum },
MOCK_KANBAN_FIELD,
);
const result = computeAggregateValueAndLabel({
data: mockData,
objectMetadataItem: mockObjectMetadata,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
});

expect(result).toEqual({
value: 2,
Expand All @@ -52,17 +54,16 @@ describe('computeAggregateValueAndLabel', () => {

it('should default to count when field not found', () => {
const mockData = {
[MOCK_KANBAN_FIELD]: {
[MOCK_KANBAN_FIELD_NAME]: {
[AGGREGATE_OPERATIONS.count]: 42,
},
};

const result = computeAggregateValueAndLabel(
mockData,
mockObjectMetadata,
{ fieldMetadataId: 'non-existent', operation: AGGREGATE_OPERATIONS.sum },
MOCK_KANBAN_FIELD,
);
const result = computeAggregateValueAndLabel({
data: mockData,
objectMetadataItem: mockObjectMetadata,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
});

expect(result).toEqual({
value: 42,
Expand All @@ -77,12 +78,12 @@ describe('computeAggregateValueAndLabel', () => {
},
};

const result = computeAggregateValueAndLabel(
mockData,
mockObjectMetadata,
{ fieldMetadataId: MOCK_FIELD_ID, operation: AGGREGATE_OPERATIONS.sum },
MOCK_KANBAN_FIELD,
);
const result = computeAggregateValueAndLabel({
data: mockData,
objectMetadataItem: mockObjectMetadata,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
});

expect(result).toEqual({
value: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { KanbanAggregateOperation } from '@/object-record/record-index/states/re
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { isDefined } from '~/utils/isDefined';

export const buildRecordGqlFieldsAggregate = ({
export const buildRecordGqlFieldsAggregateForRecordBoard = ({
objectMetadataItem,
recordIndexKanbanAggregateOperation,
kanbanFieldName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,59 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import isEmpty from 'lodash.isempty';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';

export const computeAggregateValueAndLabel = (
data: AggregateRecordsData,
objectMetadataItem: ObjectMetadataItem,
recordIndexKanbanAggregateOperation: KanbanAggregateOperation,
kanbanFieldName: string,
) => {
export const computeAggregateValueAndLabel = ({
data,
objectMetadataItem,
fieldMetadataId,
aggregateOperation,
fallbackFieldName,
}: {
data: AggregateRecordsData;
objectMetadataItem: ObjectMetadataItem;
fieldMetadataId?: string | null;
aggregateOperation?: AGGREGATE_OPERATIONS | null;
fallbackFieldName?: string;
}) => {
if (isEmpty(data)) {
return {};
}
const kanbanAggregateOperationField = objectMetadataItem.fields?.find(
(field) =>
field.id === recordIndexKanbanAggregateOperation?.fieldMetadataId,
const field = objectMetadataItem.fields?.find(
(field) => field.id === fieldMetadataId,
);

const kanbanAggregateOperationFieldName = kanbanAggregateOperationField?.name;

if (
!isDefined(kanbanAggregateOperationFieldName) ||
!isDefined(recordIndexKanbanAggregateOperation?.operation)
) {
if (!isDefined(field)) {
if (!fallbackFieldName) {
throw new Error('Missing fallback field name');
}
return {
value: data?.[kanbanFieldName]?.[AGGREGATE_OPERATIONS.count],
value: data?.[fallbackFieldName]?.[AGGREGATE_OPERATIONS.count],
label: `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`,
};
}

const aggregateValue =
data[kanbanAggregateOperationFieldName]?.[
recordIndexKanbanAggregateOperation.operation
];
if (!isDefined(aggregateOperation)) {
throw new Error('Missing aggregate operation');
}

const aggregateValue = data[field.name]?.[aggregateOperation];

const value =
isDefined(aggregateValue) &&
kanbanAggregateOperationField?.type === FieldMetadataType.Currency
isDefined(aggregateValue) && field.type === FieldMetadataType.Currency
? Number(aggregateValue) / 1_000_000
: aggregateValue;

const label =
aggregateOperation === AGGREGATE_OPERATIONS.count
? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`
: `${getAggregateOperationLabel(aggregateOperation)} of ${field.name}`;

return {
value,
label: `${getAggregateOperationLabel(recordIndexKanbanAggregateOperation.operation)} of ${kanbanAggregateOperationFieldName}`,
label,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/recor
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
import { RecordTableFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableFooter';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRef } from 'react';

const StyledTable = styled.table`
Expand All @@ -33,6 +35,10 @@ export const RecordTable = () => {

const tableBodyRef = useRef<HTMLTableElement>(null);

const isAggregateQueryEnabled = useIsFeatureEnabled(
'IS_AGGREGATE_QUERY_ENABLED',
);

const { toggleClickOutsideListener } = useClickOutsideListener(
RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
);
Expand Down Expand Up @@ -90,6 +96,7 @@ export const RecordTable = () => {
<RecordTableRecordGroupsBody />
)}
<RecordTableStickyEffect />
{isAggregateQueryEnabled && <RecordTableFooter />}
</StyledTable>
<DragSelect
dragSelectable={tableBodyRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@ export const RecordTableStickyEffect = () => {
document
.getElementById('record-table-header')
?.classList.add('first-columns-sticky');
document
.getElementById('record-table-footer')
?.classList.add('first-columns-sticky');
} else {
document
.getElementById('record-table-body')
?.classList.remove('first-columns-sticky');
document
.getElementById('record-table-header')
?.classList.remove('first-columns-sticky');
document
.getElementById('record-table-footer')
?.classList.remove('first-columns-sticky');
}
}, [scrollLeft, setIsRecordTableScrolledLeft]);

Expand Down
Loading

0 comments on commit 05cd0d1

Please sign in to comment.