Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aggregate follow-up #9547

Merged
merged 12 commits into from
Jan 10, 2025
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';

export type RecordGqlFieldsAggregate = Record<string, AGGREGATE_OPERATIONS[]>;
export type RecordGqlFieldsAggregate = Record<
string,
ExtendedAggregateOperations[]
>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGq
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-ui';

export type AggregateRecordsData = {
[fieldName: string]: {
[operation in AGGREGATE_OPERATIONS]?: string | number | undefined;
[operation in ExtendedAggregateOperations]?: string | number | undefined;
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery';
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
import { useMemo } from 'react';
Expand All @@ -10,7 +10,7 @@ import { isDefined } from 'twenty-ui';
export type GqlFieldToFieldMap = {
[gqlField: string]: [
fieldName: string,
aggregateOperation: AGGREGATE_OPERATIONS,
aggregateOperation: ExtendedAggregateOperations,
];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,15 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
export const MultipleFiltersButton = () => {
const { resetFilterDropdown } = useResetFilterDropdown();

const { isDropdownOpen, toggleDropdown } = useDropdown(
OBJECT_FILTER_DROPDOWN_ID,
);
const { toggleDropdown } = useDropdown(OBJECT_FILTER_DROPDOWN_ID);

const handleClick = () => {
toggleDropdown();
resetFilterDropdown();
Comment on lines 12 to 13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: toggling dropdown before resetting could cause visual glitches - consider resetting first

};

return (
<StyledHeaderDropdownButton
isUnfolded={isDropdownOpen}
onClick={handleClick}
>
<StyledHeaderDropdownButton onClick={handleClick}>
Filter
</StyledHeaderDropdownButton>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { SORT_DIRECTIONS } from '../types/SortDirection';
Expand Down Expand Up @@ -78,8 +77,6 @@ export const ObjectSortDropdownButton = ({

const { recordIndexId } = useRecordIndexContextOrThrow();

const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID);

const handleButtonClick = () => {
toggleSortDropdown();
};
Expand Down Expand Up @@ -141,10 +138,7 @@ export const ObjectSortDropdownButton = ({
dropdownHotkeyScope={hotkeyScope}
dropdownOffset={{ y: 8 }}
clickableComponent={
<StyledHeaderDropdownButton
isUnfolded={isDropdownOpen}
onClick={handleButtonClick}
>
<StyledHeaderDropdownButton onClick={handleButtonClick}>
Sort
</StyledHeaderDropdownButton>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record
import { RecordBoardColumnHeaderAggregateDropdownFieldsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent';
import { RecordBoardColumnHeaderAggregateDropdownMenuContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent';
import { RecordBoardColumnHeaderAggregateDropdownOptionsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent';
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';

Expand Down Expand Up @@ -41,15 +42,31 @@ export const AggregateDropdownContent = () => {
/>
);
}
case 'datesAggregateOperationOptions': {
const datesAvailableAggregations: AvailableFieldsForAggregateOperation =
getAvailableFieldsIdsForAggregationFromObjectFields(
objectMetadataItem.fields,
[
DATE_AGGREGATE_OPERATIONS.earliest,
DATE_AGGREGATE_OPERATIONS.latest,
],
Comment on lines +49 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using DATE_AGGREGATE_OPERATIONS as an array directly rather than hardcoding array elements to avoid potential sync issues if new operations are added

);
return (
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
availableAggregations={datesAvailableAggregations}
title="Dates"
/>
);
}
case 'moreAggregateOperationOptions': {
const availableAggregations: AvailableFieldsForAggregateOperation =
const availableAggregationsWithoutDates: AvailableFieldsForAggregateOperation =
getAvailableFieldsIdsForAggregationFromObjectFields(
objectMetadataItem.fields,
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS,
);
return (
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
availableAggregations={availableAggregations}
availableAggregations={availableAggregationsWithoutDates}
title="More options"
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { aggregateOperationComponentState } from '@/object-record/record-board/r
import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
Expand Down Expand Up @@ -47,9 +46,6 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
);

if (!isDefined(aggregateOperation)) return <></>;

const convertedAggregateOperation =
convertExtendedAggregateOperationToAggregateOperation(aggregateOperation);
return (
<>
<DropdownMenuHeader
Expand All @@ -75,7 +71,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
onClick={() => {
updateViewAggregate({
kanbanAggregateOperationFieldMetadataId: fieldId,
kanbanAggregateOperation: convertedAggregateOperation,
kanbanAggregateOperation: aggregateOperation,
});
closeDropdown();
}}
Expand All @@ -85,7 +81,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
recordIndexKanbanAggregateOperation?.fieldMetadataId ===
fieldId &&
recordIndexKanbanAggregateOperation?.operation ===
convertedAggregateOperation
aggregateOperation
? IconCheck
: undefined
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
text={'Percent'}
hasSubMenu
/>
<MenuItem
onClick={() => {
onContentChange('datesAggregateOperationOptions');
}}
text={'Dates'}
hasSubMenu
/>
<MenuItem
onClick={() => {
onContentChange('moreAggregateOperationOptions');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords';
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
import { FieldMetadataType } from '~/generated/graphql';

const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
Expand Down Expand Up @@ -154,17 +155,17 @@ describe('computeAggregateValueAndLabel', () => {
],
} as ObjectMetadataItem;

const mockData = {
const mockFormattedData = {
createdAt: {
[AGGREGATE_OPERATIONS.min]: '2023-01-01T12:00:00Z',
[DATE_AGGREGATE_OPERATIONS.earliest]: '2023-01-01T12:00:00Z',
},
} as AggregateRecordsData;

const result = computeAggregateValueAndLabel({
data: mockData,
data: mockFormattedData,
objectMetadataItem: mockObjectMetadataWithDatetimeField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.min,
aggregateOperation: DATE_AGGREGATE_OPERATIONS.earliest,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});
Expand All @@ -189,17 +190,17 @@ describe('computeAggregateValueAndLabel', () => {
],
} as ObjectMetadataItem;

const mockData = {
const mockFormattedData = {
updatedAt: {
[AGGREGATE_OPERATIONS.max]: '2023-12-31T23:59:59Z',
[DATE_AGGREGATE_OPERATIONS.latest]: '2023-12-31T23:59:59Z',
},
} as AggregateRecordsData;

const result = computeAggregateValueAndLabel({
data: mockData,
data: mockFormattedData,
objectMetadataItem: mockObjectMetadataWithDatetimeField,
fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.max,
aggregateOperation: DATE_AGGREGATE_OPERATIONS.latest,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
import { expect } from '@storybook/test';

describe('getAggregateOperationLabel', () => {
Expand All @@ -9,8 +10,12 @@ describe('getAggregateOperationLabel', () => {
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.avg)).toBe(
'Average',
);
expect(getAggregateOperationLabel('EARLIEST')).toBe('Earliest date');
expect(getAggregateOperationLabel('LATEST')).toBe('Latest date');
expect(getAggregateOperationLabel(DATE_AGGREGATE_OPERATIONS.earliest)).toBe(
'Earliest date',
);
expect(getAggregateOperationLabel(DATE_AGGREGATE_OPERATIONS.latest)).toBe(
'Latest date',
);
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.sum)).toBe('Sum');
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)).toBe(
'Count all',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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 { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import isEmpty from 'lodash.isempty';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { formatAmount } from '~/utils/format/formatAmount';
Expand All @@ -28,7 +28,7 @@ export const computeAggregateValueAndLabel = ({
data: AggregateRecordsData;
objectMetadataItem: ObjectMetadataItem;
fieldMetadataId?: string | null;
aggregateOperation?: AGGREGATE_OPERATIONS | null;
aggregateOperation?: ExtendedAggregateOperations | null;
fallbackFieldName?: string;
dateFormat: DateFormat;
timeFormat: TimeFormat;
Expand Down Expand Up @@ -62,11 +62,19 @@ export const computeAggregateValueAndLabel = ({

const displayAsRelativeDate = field?.settings?.displayAsRelativeDate;

if (COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) {
if (
COUNT_AGGREGATE_OPERATION_OPTIONS.includes(
aggregateOperation as AGGREGATE_OPERATIONS,
)
) {
Comment on lines +65 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: type assertion here could mask potential type mismatches between ExtendedAggregateOperations and AGGREGATE_OPERATIONS

value = aggregateValue;
} else if (!isDefined(aggregateValue)) {
value = '-';
} else if (PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) {
} else if (
PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(
aggregateOperation as AGGREGATE_OPERATIONS,
)
) {
value = `${formatNumber(Number(aggregateValue) * 100)}%`;
} else {
switch (field.type) {
Expand Down Expand Up @@ -110,16 +118,11 @@ export const computeAggregateValueAndLabel = ({
}
}
}
const convertedAggregateOperation =
convertAggregateOperationToExtendedAggregateOperation(
aggregateOperation,
field.type,
);
const label = getAggregateOperationLabel(convertedAggregateOperation);
const label = getAggregateOperationLabel(aggregateOperation);
const labelWithFieldName =
aggregateOperation === AGGREGATE_OPERATIONS.count
? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`
: `${getAggregateOperationLabel(convertedAggregateOperation)} of ${field.label}`;
: `${getAggregateOperationLabel(aggregateOperation)} of ${field.label}`;

return {
value,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';

export const getAggregateOperationLabel = (
Expand All @@ -25,9 +26,9 @@ export const getAggregateOperationLabel = (
return 'Percent empty';
case AGGREGATE_OPERATIONS.percentageNotEmpty:
return 'Percent not empty';
case 'EARLIEST':
case DATE_AGGREGATE_OPERATIONS.earliest:
return 'Earliest date';
case 'LATEST':
case DATE_AGGREGATE_OPERATIONS.latest:
return 'Latest date';
default:
throw new Error(`Unknown aggregate operation: ${operation}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export type RecordBoardColumnHeaderAggregateContentId =
| 'aggregateFields'
| 'countAggregateOperationsOptions'
| 'percentAggregateOperationsOptions'
| 'datesAggregateOperationOptions'
| 'moreAggregateOperationOptions';
Loading
Loading