Aggregate count variations (#9304)

Closes https://github.com/twentyhq/private-issues/issues/222

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Marie
2025-01-02 17:35:05 +01:00
committed by GitHub
parent 0f1458cbe9
commit 5d857fbfb5
43 changed files with 650 additions and 203 deletions

View File

@ -5,10 +5,14 @@ import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateReco
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery';
import { renderHook } from '@testing-library/react';
import { getColumnNameForAggregateOperation } from 'twenty-shared';
import { FieldMetadataType } from '~/generated/graphql';
jest.mock('@/object-metadata/hooks/useObjectMetadataItem');
jest.mock('@/object-record/utils/generateAggregateQuery');
jest.mock('twenty-shared', () => ({
getColumnNameForAggregateOperation: jest.fn(),
}));
const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'company',
@ -65,6 +69,7 @@ describe('useAggregateRecordsQuery', () => {
});
it('should handle simple count operation', () => {
(getColumnNameForAggregateOperation as jest.Mock).mockReturnValue('name');
const { result } = renderHook(() =>
useAggregateRecordsQuery({
objectNameSingular: 'company',
@ -86,6 +91,7 @@ describe('useAggregateRecordsQuery', () => {
});
it('should handle field aggregation', () => {
(getColumnNameForAggregateOperation as jest.Mock).mockReturnValue('amount');
const { result } = renderHook(() =>
useAggregateRecordsQuery({
objectNameSingular: 'company',

View File

@ -140,8 +140,8 @@ describe('computeAggregateValueAndLabel', () => {
expect(result).toEqual({
value: 42,
label: 'Count',
labelWithFieldName: 'Count',
label: 'Count all',
labelWithFieldName: 'Count all',
});
});

View File

@ -10,8 +10,23 @@ describe('getAggregateOperationLabel', () => {
);
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.sum)).toBe('Sum');
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)).toBe(
'Count',
'Count all',
);
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.countEmpty)).toBe(
'Count empty',
);
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.countNotEmpty)).toBe(
'Count not empty',
);
expect(
getAggregateOperationLabel(AGGREGATE_OPERATIONS.countUniqueValues),
).toBe('Count unique values');
expect(
getAggregateOperationLabel(AGGREGATE_OPERATIONS.percentageEmpty),
).toBe('Percent empty');
expect(
getAggregateOperationLabel(AGGREGATE_OPERATIONS.percentageNotEmpty),
).toBe('Percent not empty');
});
it('should throw error for unknown operation', () => {

View File

@ -2,6 +2,8 @@ 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 { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
import isEmpty from 'lodash.isempty';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { formatAmount } from '~/utils/format/formatAmount';
@ -47,10 +49,12 @@ export const computeAggregateValueAndLabel = ({
let value;
if (aggregateOperation === AGGREGATE_OPERATIONS.count) {
if (COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) {
value = aggregateValue;
} else if (!isDefined(aggregateValue)) {
value = '-';
} else if (PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) {
value = `${formatNumber(Number(aggregateValue) * 100)}%`;
} else {
value = Number(aggregateValue);

View File

@ -11,7 +11,17 @@ export const getAggregateOperationLabel = (operation: AGGREGATE_OPERATIONS) => {
case AGGREGATE_OPERATIONS.sum:
return 'Sum';
case AGGREGATE_OPERATIONS.count:
return 'Count';
return 'Count all';
case AGGREGATE_OPERATIONS.countEmpty:
return 'Count empty';
case AGGREGATE_OPERATIONS.countNotEmpty:
return 'Count not empty';
case AGGREGATE_OPERATIONS.countUniqueValues:
return 'Count unique values';
case AGGREGATE_OPERATIONS.percentageEmpty:
return 'Percent empty';
case AGGREGATE_OPERATIONS.percentageNotEmpty:
return 'Percent not empty';
default:
throw new Error(`Unknown aggregate operation: ${operation}`);
}

View File

@ -1,4 +1,5 @@
export type RecordBoardColumnHeaderAggregateContentId =
| 'aggregateOperations'
| 'aggregateFields'
| 'countAggregateOperationsOptions'
| 'moreAggregateOperationOptions';

View File

@ -4,4 +4,9 @@ export enum AGGREGATE_OPERATIONS {
avg = 'AVG',
sum = 'SUM',
count = 'COUNT',
countEmpty = 'COUNT_EMPTY',
countNotEmpty = 'COUNT_NOT_EMPTY',
countUniqueValues = 'COUNT_UNIQUE_VALUES',
percentageEmpty = 'PERCENTAGE_EMPTY',
percentageNotEmpty = 'PERCENTAGE_NOT_EMPTY',
}

View File

@ -0,0 +1,45 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContext } from 'react';
import { Key } from 'ts-key-enum';
import { IconChevronLeft } from 'twenty-ui';
export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({
aggregateOperations,
title,
}: {
aggregateOperations: AGGREGATE_OPERATIONS[];
title: string;
}) => {
const { dropdownId, resetContent } = useContext(
RecordTableColumnAggregateFooterDropdownContext,
);
const { closeDropdown } = useDropdown(dropdownId);
useScopedHotkeys(
[Key.Escape],
() => {
resetContent();
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
{title}
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<RecordTableColumnAggregateFooterAggregateOperationMenuItems
aggregateOperations={aggregateOperations}
/>
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -2,7 +2,6 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record-
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ReactNode, useContext } from 'react';
import { IconCheck, isDefined, MenuItem } from 'twenty-ui';
@ -24,7 +23,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
);
const { closeDropdown } = useDropdown(dropdownId);
return (
<DropdownMenuItemsContainer>
<>
{aggregateOperations.map((operation) => (
<MenuItem
key={operation}
@ -55,6 +54,6 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
}
aria-selected={!isDefined(currentViewFieldAggregateOperation)}
/>
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -1,16 +1,67 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterDropdownSubmenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { RecordTableColumnAggregateFooterDropdownMoreOptionsContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownMoreOptionsContent';
import { RecordTableColumnAggregateFooterMenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent';
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
export const RecordTableColumnAggregateFooterDropdownContent = () => {
const { currentContentId } = useDropdown({
const { currentContentId, fieldMetadataId } = useDropdown({
context: RecordTableColumnAggregateFooterDropdownContext,
});
const { objectMetadataItem } = useRecordTableContextOrThrow();
const fieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === fieldMetadataId,
);
const availableAggregateOperations =
getAvailableAggregateOperationsForFieldMetadataType({
fieldMetadataType: fieldMetadata?.type,
});
switch (currentContentId) {
case 'moreAggregateOperationOptions':
return <RecordTableColumnAggregateFooterDropdownMoreOptionsContent />;
case 'moreAggregateOperationOptions': {
const aggregateOperations = availableAggregateOperations.filter(
(aggregateOperation) =>
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
);
return (
<RecordTableColumnAggregateFooterDropdownSubmenuContent
aggregateOperations={aggregateOperations}
title="More options"
/>
);
}
case 'countAggregateOperationsOptions': {
const aggregateOperations = availableAggregateOperations.filter(
(aggregateOperation) =>
COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
);
return (
<RecordTableColumnAggregateFooterDropdownSubmenuContent
aggregateOperations={aggregateOperations}
title="Count"
/>
);
}
case 'percentAggregateOperationsOptions': {
const aggregateOperations = availableAggregateOperations.filter(
(aggregateOperation) =>
PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
);
return (
<RecordTableColumnAggregateFooterDropdownSubmenuContent
aggregateOperations={aggregateOperations}
title="Percent"
/>
);
}
default:
return <RecordTableColumnAggregateFooterMenuContent />;
}

View File

@ -1,57 +0,0 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContext, useMemo } from 'react';
import { Key } from 'ts-key-enum';
import { IconChevronLeft } from 'twenty-ui';
export const RecordTableColumnAggregateFooterDropdownMoreOptionsContent =
() => {
const { fieldMetadataId, dropdownId, resetContent } = useContext(
RecordTableColumnAggregateFooterDropdownContext,
);
const { closeDropdown } = useDropdown(dropdownId);
const { objectMetadataItem } = useRecordTableContextOrThrow();
useScopedHotkeys(
[Key.Escape],
() => {
resetContent();
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const availableAggregateOperations = useMemo(
() =>
getAvailableAggregateOperationsForFieldMetadataType({
fieldMetadataType: objectMetadataItem.fields.find(
(field) => field.id === fieldMetadataId,
)?.type,
}).filter(
(aggregateOperation) =>
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
),
[fieldMetadataId, objectMetadataItem.fields],
);
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
More options
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<RecordTableColumnAggregateFooterAggregateOperationMenuItems
aggregateOperations={availableAggregateOperations}
/>
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -1,5 +1,4 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
@ -10,6 +9,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContext, useMemo } from 'react';
import { Key } from 'ts-key-enum';
import { MenuItem } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const RecordTableColumnAggregateFooterMenuContent = () => {
const { fieldMetadataId, dropdownId, onContentChange } = useContext(
@ -36,32 +36,43 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
[fieldMetadataId, objectMetadataItem.fields],
);
const standardAvailableAggregateOperation =
availableAggregateOperation.filter((aggregateOperation) =>
STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
);
const otherAvailableAggregateOperation = availableAggregateOperation.filter(
(aggregateOperation) =>
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation),
);
const fieldIsRelation =
objectMetadataItem.fields.find((field) => field.id === fieldMetadataId)
?.type === FieldMetadataType.Relation;
return (
<>
<DropdownMenuItemsContainer>
<RecordTableColumnAggregateFooterAggregateOperationMenuItems
aggregateOperations={standardAvailableAggregateOperation}
>
{otherAvailableAggregateOperation.length > 0 ? (
<MenuItem
key={'more-options'}
onClick={() => {
onContentChange('moreAggregateOperationOptions');
}}
text={'More options'}
hasSubMenu
/>
) : null}
</RecordTableColumnAggregateFooterAggregateOperationMenuItems>
<MenuItem
onClick={() => {
onContentChange('countAggregateOperationsOptions');
}}
text={'Count'}
hasSubMenu
/>
{!fieldIsRelation && (
<MenuItem
onClick={() => {
onContentChange('percentAggregateOperationsOptions');
}}
text={'Percent'}
hasSubMenu
/>
)}
{otherAvailableAggregateOperation.length > 0 ? (
<MenuItem
onClick={() => {
onContentChange('moreAggregateOperationOptions');
}}
text={'More options'}
hasSubMenu
/>
) : null}
</DropdownMenuItemsContainer>
</>
);

View File

@ -1,6 +1,6 @@
import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-ui';
import { isDefined, OverflowingTextWithTooltip } from 'twenty-ui';
const StyledText = styled.span`
overflow: hidden;
@ -23,13 +23,7 @@ const StyledValueContainer = styled.div`
gap: 4px;
height: 32px;
justify-content: flex-end;
padding: 8px;
`;
const StyledLabel = styled.div`
align-items: center;
display: flex;
gap: 4px;
padding: 0 8px;
`;
const StyledValue = styled.div`
@ -57,7 +51,7 @@ export const RecordTableColumnAggregateFooterValue = ({
<></>
) : (
<>
<StyledLabel>{aggregateLabel}</StyledLabel>
<OverflowingTextWithTooltip text={aggregateLabel} />
<StyledValue>{aggregateValue}</StyledValue>
</>
)}

View File

@ -0,0 +1,8 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
export const COUNT_AGGREGATE_OPERATION_OPTIONS = [
AGGREGATE_OPERATIONS.count,
AGGREGATE_OPERATIONS.countEmpty,
AGGREGATE_OPERATIONS.countNotEmpty,
AGGREGATE_OPERATIONS.countUniqueValues,
];

View File

@ -0,0 +1,6 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
export const PERCENT_AGGREGATE_OPERATION_OPTIONS = [
AGGREGATE_OPERATIONS.percentageEmpty,
AGGREGATE_OPERATIONS.percentageNotEmpty,
];

View File

@ -1,5 +1,7 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
export const STANDARD_AGGREGATE_OPERATION_OPTIONS = [
AGGREGATE_OPERATIONS.count,
...COUNT_AGGREGATE_OPERATION_OPTIONS,
...PERCENT_AGGREGATE_OPERATION_OPTIONS,
];

View File

@ -1,2 +1,4 @@
export type RecordTableFooterAggregateContentId =
'moreAggregateOperationOptions';
| 'moreAggregateOperationOptions'
| 'countAggregateOperationsOptions'
| 'percentAggregateOperationsOptions';

View File

@ -1,6 +1,6 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldsAvailableByAggregateOperation';
import { AggregateOperationsOmittingCount } from '@/object-record/types/AggregateOperationsOmittingCount';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
@ -10,8 +10,17 @@ export const getAvailableAggregateOperationsForFieldMetadataType = ({
}: {
fieldMetadataType?: FieldMetadataType;
}) => {
if (fieldMetadataType === FieldMetadataType.Relation) {
return [AGGREGATE_OPERATIONS.count];
}
const availableAggregateOperations = new Set<AGGREGATE_OPERATIONS>([
AGGREGATE_OPERATIONS.count,
AGGREGATE_OPERATIONS.countEmpty,
AGGREGATE_OPERATIONS.countNotEmpty,
AGGREGATE_OPERATIONS.countUniqueValues,
AGGREGATE_OPERATIONS.percentageEmpty,
AGGREGATE_OPERATIONS.percentageNotEmpty,
]);
if (!isDefined(fieldMetadataType)) {
@ -22,7 +31,7 @@ export const getAvailableAggregateOperationsForFieldMetadataType = ({
.filter((operation) =>
isFieldTypeValidForAggregateOperation(
fieldMetadataType,
operation as AggregateOperationsOmittingCount,
operation as AggregateOperationsOmittingStandardOperations,
),
)
.forEach((operation) =>

View File

@ -1,6 +0,0 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
export type AggregateOperationsOmittingCount = Exclude<
AGGREGATE_OPERATIONS,
AGGREGATE_OPERATIONS.count
>;

View File

@ -0,0 +1,11 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
export type AggregateOperationsOmittingStandardOperations = Exclude<
AGGREGATE_OPERATIONS,
| AGGREGATE_OPERATIONS.count
| AGGREGATE_OPERATIONS.countEmpty
| AGGREGATE_OPERATIONS.countNotEmpty
| AGGREGATE_OPERATIONS.countUniqueValues
| AGGREGATE_OPERATIONS.percentageEmpty
| AGGREGATE_OPERATIONS.percentageNotEmpty
>;

View File

@ -1,5 +1,5 @@
import { AggregateOperationsOmittingCount } from '@/object-record/types/AggregateOperationsOmittingCount';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
export type AvailableFieldsForAggregateOperation = {
[T in AggregateOperationsOmittingCount]?: string[];
[T in AggregateOperationsOmittingStandardOperations]?: string[];
};

View File

@ -1,5 +1,7 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
import { FieldMetadataType } from '~/generated/graphql';
@ -43,8 +45,10 @@ describe('getAvailableFieldsIdsForAggregationFromObjectFields', () => {
]);
Object.values(AGGREGATE_OPERATIONS).forEach((operation) => {
if (operation !== AGGREGATE_OPERATIONS.count) {
expect(result[operation]).toEqual([]);
if (!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(operation)) {
expect(
result[operation as AggregateOperationsOmittingStandardOperations],
).toEqual([]);
}
});
});
@ -53,8 +57,10 @@ describe('getAvailableFieldsIdsForAggregationFromObjectFields', () => {
const result = getAvailableFieldsIdsForAggregationFromObjectFields([]);
Object.values(AGGREGATE_OPERATIONS).forEach((operation) => {
if (operation !== AGGREGATE_OPERATIONS.count) {
expect(result[operation]).toEqual([]);
if (!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(operation)) {
expect(
result[operation as AggregateOperationsOmittingStandardOperations],
).toEqual([]);
}
});
});

View File

@ -1,6 +1,6 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldsAvailableByAggregateOperation';
import { AggregateOperationsOmittingCount } from '@/object-record/types/AggregateOperationsOmittingCount';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
describe('initializeAvailableFieldsForAggregateOperationMap', () => {
@ -18,7 +18,9 @@ describe('initializeAvailableFieldsForAggregateOperationMap', () => {
it('should not include count operation', () => {
const result = initializeAvailableFieldsForAggregateOperationMap();
expect(
result[AGGREGATE_OPERATIONS.count as AggregateOperationsOmittingCount],
result[
AGGREGATE_OPERATIONS.count as AggregateOperationsOmittingStandardOperations
],
).toBeUndefined();
});
});

View File

@ -1,5 +1,5 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { AggregateOperationsOmittingCount } from '@/object-record/types/AggregateOperationsOmittingCount';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation';
import { FieldMetadataType } from '~/generated/graphql';
@ -49,7 +49,7 @@ describe('isFieldTypeValidForAggregateOperation', () => {
expect(
isFieldTypeValidForAggregateOperation(
numericField,
operation as AggregateOperationsOmittingCount,
operation as AggregateOperationsOmittingStandardOperations,
),
).toBe(true);
});

View File

@ -1,5 +1,6 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { getColumnNameForAggregateOperation } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { capitalize } from '~/utils/string/capitalize';
@ -15,8 +16,30 @@ export const getAvailableAggregationsFromObjectFields = (
fields: FieldMetadataItem[],
): Aggregations => {
return fields.reduce<Record<string, NameForAggregation>>((acc, field) => {
if (field.type === FieldMetadataType.Relation) {
acc[field.name] = {
[AGGREGATE_OPERATIONS.count]: 'totalCount',
};
return acc;
}
const columnName = getColumnNameForAggregateOperation(
field.name,
field.type,
);
acc[field.name] = {
[AGGREGATE_OPERATIONS.countUniqueValues]: `countUniqueValues${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.countEmpty]: `countEmpty${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.countNotEmpty]: `countNotEmpty${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.percentageEmpty]: `percentageEmpty${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.percentageNotEmpty]: `percentageNotEmpty${capitalize(columnName)}`,
[AGGREGATE_OPERATIONS.count]: 'totalCount',
};
if (field.type === FieldMetadataType.DateTime) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`,
};
@ -24,6 +47,7 @@ export const getAvailableAggregationsFromObjectFields = (
if (field.type === FieldMetadataType.Number) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.avg]: `avg${capitalize(field.name)}`,
@ -33,6 +57,7 @@ export const getAvailableAggregationsFromObjectFields = (
if (field.type === FieldMetadataType.Currency) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}AmountMicros`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}AmountMicros`,
[AGGREGATE_OPERATIONS.avg]: `avg${capitalize(field.name)}AmountMicros`,
@ -44,8 +69,6 @@ export const getAvailableAggregationsFromObjectFields = (
acc[field.name] = {};
}
acc[field.name][AGGREGATE_OPERATIONS.count] = 'totalCount';
return acc;
}, {});
};

View File

@ -1,6 +1,6 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldsAvailableByAggregateOperation';
import { AggregateOperationsOmittingCount } from '@/object-record/types/AggregateOperationsOmittingCount';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation';
@ -14,7 +14,7 @@ export const getAvailableFieldsIdsForAggregationFromObjectFields = (
Object.keys(FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION).forEach(
(aggregateOperation) => {
const typedAggregateOperation =
aggregateOperation as AggregateOperationsOmittingCount;
aggregateOperation as AggregateOperationsOmittingStandardOperations;
if (
isFieldTypeValidForAggregateOperation(

View File

@ -1,10 +1,10 @@
import { FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldsAvailableByAggregateOperation';
import { AggregateOperationsOmittingCount } from '@/object-record/types/AggregateOperationsOmittingCount';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const isFieldTypeValidForAggregateOperation = (
fieldType: FieldMetadataType,
aggregateOperation: AggregateOperationsOmittingCount,
aggregateOperation: AggregateOperationsOmittingStandardOperations,
): boolean => {
return FIELDS_AVAILABLE_BY_AGGREGATE_OPERATION[aggregateOperation].includes(
fieldType,