Aggregate follow-up (#9547)

In this PR
- fix [some UI
regressions](https://discord.com/channels/1130383047699738754/1327189577575956514/1327189577575956514)
introduced by work on view groups
- address some follow-ups:
1. [Menu should keep selected when the menu is
open](https://discord.com/channels/1130383047699738754/1326607851824877639/1326607851824877639)
2.
[Cropping](https://discord.com/channels/1130383047699738754/1326610578869063800/1326610578869063800)
3. [Put earliest date / latest date in a separate "Date"
submenu](https://discord.com/channels/1130383047699738754/1326856023985618966/1326856023985618966)
- Refactor around date aggregate operations
This commit is contained in:
Marie
2025-01-10 20:01:36 +01:00
committed by GitHub
parent 873f20bc0e
commit 2e0169b954
48 changed files with 440 additions and 195 deletions

View File

@ -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[]
>;

View File

@ -5,13 +5,13 @@ import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGq
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; 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 isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
export type AggregateRecordsData = { export type AggregateRecordsData = {
[fieldName: string]: { [fieldName: string]: {
[operation in AGGREGATE_OPERATIONS]?: string | number | undefined; [operation in ExtendedAggregateOperations]?: string | number | undefined;
}; };
}; };

View File

@ -1,7 +1,7 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate'; 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 { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery';
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields'; import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
import { useMemo } from 'react'; import { useMemo } from 'react';
@ -10,7 +10,7 @@ import { isDefined } from 'twenty-ui';
export type GqlFieldToFieldMap = { export type GqlFieldToFieldMap = {
[gqlField: string]: [ [gqlField: string]: [
fieldName: string, fieldName: string,
aggregateOperation: AGGREGATE_OPERATIONS, aggregateOperation: ExtendedAggregateOperations,
]; ];
}; };

View File

@ -6,9 +6,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
export const MultipleFiltersButton = () => { export const MultipleFiltersButton = () => {
const { resetFilterDropdown } = useResetFilterDropdown(); const { resetFilterDropdown } = useResetFilterDropdown();
const { isDropdownOpen, toggleDropdown } = useDropdown( const { toggleDropdown } = useDropdown(OBJECT_FILTER_DROPDOWN_ID);
OBJECT_FILTER_DROPDOWN_ID,
);
const handleClick = () => { const handleClick = () => {
toggleDropdown(); toggleDropdown();
@ -16,10 +14,7 @@ export const MultipleFiltersButton = () => {
}; };
return ( return (
<StyledHeaderDropdownButton <StyledHeaderDropdownButton onClick={handleClick}>
isUnfolded={isDropdownOpen}
onClick={handleClick}
>
Filter Filter
</StyledHeaderDropdownButton> </StyledHeaderDropdownButton>
); );

View File

@ -13,7 +13,6 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; 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 { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { SORT_DIRECTIONS } from '../types/SortDirection'; import { SORT_DIRECTIONS } from '../types/SortDirection';
@ -78,8 +77,6 @@ export const ObjectSortDropdownButton = ({
const { recordIndexId } = useRecordIndexContextOrThrow(); const { recordIndexId } = useRecordIndexContextOrThrow();
const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID);
const handleButtonClick = () => { const handleButtonClick = () => {
toggleSortDropdown(); toggleSortDropdown();
}; };
@ -141,10 +138,7 @@ export const ObjectSortDropdownButton = ({
dropdownHotkeyScope={hotkeyScope} dropdownHotkeyScope={hotkeyScope}
dropdownOffset={{ y: 8 }} dropdownOffset={{ y: 8 }}
clickableComponent={ clickableComponent={
<StyledHeaderDropdownButton <StyledHeaderDropdownButton onClick={handleButtonClick}>
isUnfolded={isDropdownOpen}
onClick={handleButtonClick}
>
Sort Sort
</StyledHeaderDropdownButton> </StyledHeaderDropdownButton>
} }

View File

@ -3,9 +3,10 @@ import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record
import { RecordBoardColumnHeaderAggregateDropdownFieldsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent'; 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 { RecordBoardColumnHeaderAggregateDropdownMenuContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent';
import { RecordBoardColumnHeaderAggregateDropdownOptionsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent'; 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 { 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 { 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 { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields'; import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
@ -41,15 +42,31 @@ export const AggregateDropdownContent = () => {
/> />
); );
} }
case 'datesAggregateOperationOptions': {
const datesAvailableAggregations: AvailableFieldsForAggregateOperation =
getAvailableFieldsIdsForAggregationFromObjectFields(
objectMetadataItem.fields,
[
DATE_AGGREGATE_OPERATIONS.earliest,
DATE_AGGREGATE_OPERATIONS.latest,
],
);
return (
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
availableAggregations={datesAvailableAggregations}
title="Dates"
/>
);
}
case 'moreAggregateOperationOptions': { case 'moreAggregateOperationOptions': {
const availableAggregations: AvailableFieldsForAggregateOperation = const availableAggregationsWithoutDates: AvailableFieldsForAggregateOperation =
getAvailableFieldsIdsForAggregationFromObjectFields( getAvailableFieldsIdsForAggregationFromObjectFields(
objectMetadataItem.fields, objectMetadataItem.fields,
NON_STANDARD_AGGREGATE_OPERATION_OPTIONS, NON_STANDARD_AGGREGATE_OPERATION_OPTIONS,
); );
return ( return (
<RecordBoardColumnHeaderAggregateDropdownOptionsContent <RecordBoardColumnHeaderAggregateDropdownOptionsContent
availableAggregations={availableAggregations} availableAggregations={availableAggregationsWithoutDates}
title="More options" title="More options"
/> />
); );

View File

@ -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 { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; 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 { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -47,9 +46,6 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
); );
if (!isDefined(aggregateOperation)) return <></>; if (!isDefined(aggregateOperation)) return <></>;
const convertedAggregateOperation =
convertExtendedAggregateOperationToAggregateOperation(aggregateOperation);
return ( return (
<> <>
<DropdownMenuHeader <DropdownMenuHeader
@ -75,7 +71,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
onClick={() => { onClick={() => {
updateViewAggregate({ updateViewAggregate({
kanbanAggregateOperationFieldMetadataId: fieldId, kanbanAggregateOperationFieldMetadataId: fieldId,
kanbanAggregateOperation: convertedAggregateOperation, kanbanAggregateOperation: aggregateOperation,
}); });
closeDropdown(); closeDropdown();
}} }}
@ -85,7 +81,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
recordIndexKanbanAggregateOperation?.fieldMetadataId === recordIndexKanbanAggregateOperation?.fieldMetadataId ===
fieldId && fieldId &&
recordIndexKanbanAggregateOperation?.operation === recordIndexKanbanAggregateOperation?.operation ===
convertedAggregateOperation aggregateOperation
? IconCheck ? IconCheck
: undefined : undefined
} }

View File

@ -42,6 +42,13 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
text={'Percent'} text={'Percent'}
hasSubMenu hasSubMenu
/> />
<MenuItem
onClick={() => {
onContentChange('datesAggregateOperationOptions');
}}
text={'Dates'}
hasSubMenu
/>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
onContentChange('moreAggregateOperationOptions'); onContentChange('moreAggregateOperationOptions');

View File

@ -5,6 +5,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'; import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords';
import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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'; import { FieldMetadataType } from '~/generated/graphql';
const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a'; const MOCK_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a';
@ -154,17 +155,17 @@ describe('computeAggregateValueAndLabel', () => {
], ],
} as ObjectMetadataItem; } as ObjectMetadataItem;
const mockData = { const mockFormattedData = {
createdAt: { createdAt: {
[AGGREGATE_OPERATIONS.min]: '2023-01-01T12:00:00Z', [DATE_AGGREGATE_OPERATIONS.earliest]: '2023-01-01T12:00:00Z',
}, },
} as AggregateRecordsData; } as AggregateRecordsData;
const result = computeAggregateValueAndLabel({ const result = computeAggregateValueAndLabel({
data: mockData, data: mockFormattedData,
objectMetadataItem: mockObjectMetadataWithDatetimeField, objectMetadataItem: mockObjectMetadataWithDatetimeField,
fieldMetadataId: MOCK_FIELD_ID, fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.min, aggregateOperation: DATE_AGGREGATE_OPERATIONS.earliest,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME, fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams, ...defaultParams,
}); });
@ -189,17 +190,17 @@ describe('computeAggregateValueAndLabel', () => {
], ],
} as ObjectMetadataItem; } as ObjectMetadataItem;
const mockData = { const mockFormattedData = {
updatedAt: { updatedAt: {
[AGGREGATE_OPERATIONS.max]: '2023-12-31T23:59:59Z', [DATE_AGGREGATE_OPERATIONS.latest]: '2023-12-31T23:59:59Z',
}, },
} as AggregateRecordsData; } as AggregateRecordsData;
const result = computeAggregateValueAndLabel({ const result = computeAggregateValueAndLabel({
data: mockData, data: mockFormattedData,
objectMetadataItem: mockObjectMetadataWithDatetimeField, objectMetadataItem: mockObjectMetadataWithDatetimeField,
fieldMetadataId: MOCK_FIELD_ID, fieldMetadataId: MOCK_FIELD_ID,
aggregateOperation: AGGREGATE_OPERATIONS.max, aggregateOperation: DATE_AGGREGATE_OPERATIONS.latest,
fallbackFieldName: MOCK_KANBAN_FIELD_NAME, fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
...defaultParams, ...defaultParams,
}); });

View File

@ -1,5 +1,6 @@
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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'; import { expect } from '@storybook/test';
describe('getAggregateOperationLabel', () => { describe('getAggregateOperationLabel', () => {
@ -9,8 +10,12 @@ describe('getAggregateOperationLabel', () => {
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.avg)).toBe( expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.avg)).toBe(
'Average', 'Average',
); );
expect(getAggregateOperationLabel('EARLIEST')).toBe('Earliest date'); expect(getAggregateOperationLabel(DATE_AGGREGATE_OPERATIONS.earliest)).toBe(
expect(getAggregateOperationLabel('LATEST')).toBe('Latest date'); 'Earliest date',
);
expect(getAggregateOperationLabel(DATE_AGGREGATE_OPERATIONS.latest)).toBe(
'Latest date',
);
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.sum)).toBe('Sum'); expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.sum)).toBe('Sum');
expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)).toBe( expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)).toBe(
'Count all', 'Count all',

View File

@ -5,8 +5,8 @@ import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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 { 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 { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { formatAmount } from '~/utils/format/formatAmount'; import { formatAmount } from '~/utils/format/formatAmount';
@ -28,7 +28,7 @@ export const computeAggregateValueAndLabel = ({
data: AggregateRecordsData; data: AggregateRecordsData;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
fieldMetadataId?: string | null; fieldMetadataId?: string | null;
aggregateOperation?: AGGREGATE_OPERATIONS | null; aggregateOperation?: ExtendedAggregateOperations | null;
fallbackFieldName?: string; fallbackFieldName?: string;
dateFormat: DateFormat; dateFormat: DateFormat;
timeFormat: TimeFormat; timeFormat: TimeFormat;
@ -62,11 +62,19 @@ export const computeAggregateValueAndLabel = ({
const displayAsRelativeDate = field?.settings?.displayAsRelativeDate; const displayAsRelativeDate = field?.settings?.displayAsRelativeDate;
if (COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) { if (
COUNT_AGGREGATE_OPERATION_OPTIONS.includes(
aggregateOperation as AGGREGATE_OPERATIONS,
)
) {
value = aggregateValue; value = aggregateValue;
} else if (!isDefined(aggregateValue)) { } else if (!isDefined(aggregateValue)) {
value = '-'; 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)}%`; value = `${formatNumber(Number(aggregateValue) * 100)}%`;
} else { } else {
switch (field.type) { switch (field.type) {
@ -110,16 +118,11 @@ export const computeAggregateValueAndLabel = ({
} }
} }
} }
const convertedAggregateOperation = const label = getAggregateOperationLabel(aggregateOperation);
convertAggregateOperationToExtendedAggregateOperation(
aggregateOperation,
field.type,
);
const label = getAggregateOperationLabel(convertedAggregateOperation);
const labelWithFieldName = const labelWithFieldName =
aggregateOperation === AGGREGATE_OPERATIONS.count aggregateOperation === AGGREGATE_OPERATIONS.count
? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}` ? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`
: `${getAggregateOperationLabel(convertedAggregateOperation)} of ${field.label}`; : `${getAggregateOperationLabel(aggregateOperation)} of ${field.label}`;
return { return {
value, value,

View File

@ -1,4 +1,5 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
export const getAggregateOperationLabel = ( export const getAggregateOperationLabel = (
@ -25,9 +26,9 @@ export const getAggregateOperationLabel = (
return 'Percent empty'; return 'Percent empty';
case AGGREGATE_OPERATIONS.percentageNotEmpty: case AGGREGATE_OPERATIONS.percentageNotEmpty:
return 'Percent not empty'; return 'Percent not empty';
case 'EARLIEST': case DATE_AGGREGATE_OPERATIONS.earliest:
return 'Earliest date'; return 'Earliest date';
case 'LATEST': case DATE_AGGREGATE_OPERATIONS.latest:
return 'Latest date'; return 'Latest date';
default: default:
throw new Error(`Unknown aggregate operation: ${operation}`); throw new Error(`Unknown aggregate operation: ${operation}`);

View File

@ -3,4 +3,5 @@ export type RecordBoardColumnHeaderAggregateContentId =
| 'aggregateFields' | 'aggregateFields'
| 'countAggregateOperationsOptions' | 'countAggregateOperationsOptions'
| 'percentAggregateOperationsOptions' | 'percentAggregateOperationsOptions'
| 'datesAggregateOperationOptions'
| 'moreAggregateOperationOptions'; | 'moreAggregateOperationOptions';

View File

@ -1,5 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { isDefined } from '~/utils/isDefined';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown'; import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
@ -31,6 +32,7 @@ import { RecordIndexTableContainerEffect } from '@/object-record/record-index/co
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; 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 { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewBar } from '@/views/components/ViewBar'; import { ViewBar } from '@/views/components/ViewBar';
import { ViewField } from '@/views/types/ViewField'; import { ViewField } from '@/views/types/ViewField';
@ -125,6 +127,9 @@ export const RecordIndexContainer = () => {
} }
for (const viewField of viewFields) { for (const viewField of viewFields) {
const viewFieldMetadataType = objectMetadataItem.fields?.find(
(field) => field.id === viewField.fieldMetadataId,
)?.type;
const aggregateOperationForViewField = snapshot const aggregateOperationForViewField = snapshot
.getLoadable( .getLoadable(
viewFieldAggregateOperationState({ viewFieldAggregateOperationState({
@ -133,17 +138,29 @@ export const RecordIndexContainer = () => {
) )
.getValue(); .getValue();
if (aggregateOperationForViewField !== viewField.aggregateOperation) { const convertedViewFieldAggregateOperation = isDefined(
viewField.aggregateOperation,
)
? convertAggregateOperationToExtendedAggregateOperation(
viewField.aggregateOperation,
viewFieldMetadataType,
)
: viewField.aggregateOperation;
if (
aggregateOperationForViewField !==
convertedViewFieldAggregateOperation
) {
set( set(
viewFieldAggregateOperationState({ viewFieldAggregateOperationState({
viewFieldId: viewField.id, viewFieldId: viewField.id,
}), }),
viewField.aggregateOperation, convertedViewFieldAggregateOperation,
); );
} }
} }
}, },
[columnDefinitions, setTableColumns], [columnDefinitions, objectMetadataItem.fields, setTableColumns],
); );
const onViewGroupsChange = useCallback( const onViewGroupsChange = useCallback(
@ -220,8 +237,18 @@ export const RecordIndexContainer = () => {
setRecordIndexViewKanbanFieldMetadataIdState( setRecordIndexViewKanbanFieldMetadataIdState(
view.kanbanFieldMetadataId, view.kanbanFieldMetadataId,
); );
const kanbanAggregateOperationFieldMetadataType =
objectMetadataItem.fields?.find(
(field) =>
field.id === view.kanbanAggregateOperationFieldMetadataId,
)?.type;
setRecordIndexViewKanbanAggregateOperationState({ setRecordIndexViewKanbanAggregateOperationState({
operation: view.kanbanAggregateOperation, operation: isDefined(view.kanbanAggregateOperation)
? convertAggregateOperationToExtendedAggregateOperation(
view.kanbanAggregateOperation,
kanbanAggregateOperationFieldMetadataType,
)
: view.kanbanAggregateOperation,
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId, fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
}); });
setRecordIndexIsCompactModeActive(view.isCompact); setRecordIndexIsCompactModeActive(view.isCompact);

View File

@ -8,9 +8,11 @@ import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/us
import { useSetRecordIndexEntityCount } from '@/object-record/record-index/hooks/useSetRecordIndexEntityCount'; import { useSetRecordIndexEntityCount } from '@/object-record/record-index/hooks/useSetRecordIndexEntityCount';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { ViewField } from '@/views/types/ViewField'; import { ViewField } from '@/views/types/ViewField';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui';
export const RecordIndexTableContainerEffect = () => { export const RecordIndexTableContainerEffect = () => {
const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow(); const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow();
@ -83,16 +85,33 @@ export const RecordIndexTableContainerEffect = () => {
) )
.getValue(); .getValue();
if (aggregateOperationForViewField !== viewField.aggregateOperation) { const viewFieldMetadataType = columnDefinitions.find(
(columnDefinition) =>
columnDefinition.fieldMetadataId === viewField.fieldMetadataId,
)?.type;
const convertedViewFieldAggregateOperation = isDefined(
viewField.aggregateOperation,
)
? convertAggregateOperationToExtendedAggregateOperation(
viewField.aggregateOperation,
viewFieldMetadataType,
)
: viewField.aggregateOperation;
if (
aggregateOperationForViewField !==
convertedViewFieldAggregateOperation
) {
set( set(
viewFieldAggregateOperationState({ viewFieldAggregateOperationState({
viewFieldId: viewField.id, viewFieldId: viewField.id,
}), }),
viewField.aggregateOperation, convertedViewFieldAggregateOperation,
); );
} }
}, },
[], [columnDefinitions],
); );
useEffect(() => { useEffect(() => {

View File

@ -1,8 +1,8 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { createState } from '@ui/utilities/state/utils/createState'; import { createState } from '@ui/utilities/state/utils/createState';
export type KanbanAggregateOperation = { export type KanbanAggregateOperation = {
operation?: AGGREGATE_OPERATIONS | null; operation?: ExtendedAggregateOperations | null;
fieldMetadataId?: string | null; fieldMetadataId?: string | null;
} | null; } | null;

View File

@ -100,7 +100,9 @@ export const RecordTable = () => {
{isAggregateQueryEnabled && {isAggregateQueryEnabled &&
!hasRecordGroups && !hasRecordGroups &&
!isRecordTableInitialLoading && !isRecordTableInitialLoading &&
allRecordIds.length > 0 && <RecordTableAggregateFooter />} allRecordIds.length > 0 && (
<RecordTableAggregateFooter endOfTableSticky />
)}
</StyledTable> </StyledTable>
<DragSelect <DragSelect
dragSelectable={tableBodyRef} dragSelectable={tableBodyRef}

View File

@ -1,7 +1,6 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow'; import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew'; import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
@ -9,16 +8,10 @@ import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-ta
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { FeatureFlagKey } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const RecordTableRecordGroupRows = () => { export const RecordTableRecordGroupRows = () => {
const isAggregateQueryEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsAggregateQueryEnabled,
);
const currentRecordGroupId = useCurrentRecordGroupId(); const currentRecordGroupId = useCurrentRecordGroupId();
const allRecordIds = useRecoilComponentValueV2( const allRecordIds = useRecoilComponentValueV2(
@ -66,12 +59,6 @@ export const RecordTableRecordGroupRows = () => {
<RecordTablePendingRecordGroupRow /> <RecordTablePendingRecordGroupRow />
<RecordTableRecordGroupSectionAddNew /> <RecordTableRecordGroupSectionAddNew />
<RecordTableRecordGroupSectionLoadMore /> <RecordTableRecordGroupSectionLoadMore />
{isAggregateQueryEnabled && (
<RecordTableAggregateFooter
key={currentRecordGroupId}
currentRecordGroupId={currentRecordGroupId}
/>
)}
</> </>
); );
}; };

View File

@ -0,0 +1,4 @@
export enum DATE_AGGREGATE_OPERATIONS {
earliest = 'EARLIEST',
latest = 'LATEST',
}

View File

@ -1,18 +1,15 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = { export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = {
[AGGREGATE_OPERATIONS.min]: [ [AGGREGATE_OPERATIONS.min]: [
FieldMetadataType.Number, FieldMetadataType.Number,
FieldMetadataType.Currency, FieldMetadataType.Currency,
FieldMetadataType.DateTime,
FieldMetadataType.Date,
], ],
[AGGREGATE_OPERATIONS.max]: [ [AGGREGATE_OPERATIONS.max]: [
FieldMetadataType.Number, FieldMetadataType.Number,
FieldMetadataType.Currency, FieldMetadataType.Currency,
FieldMetadataType.DateTime,
FieldMetadataType.Date,
], ],
[AGGREGATE_OPERATIONS.avg]: [ [AGGREGATE_OPERATIONS.avg]: [
FieldMetadataType.Number, FieldMetadataType.Number,
@ -22,4 +19,12 @@ export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = {
FieldMetadataType.Number, FieldMetadataType.Number,
FieldMetadataType.Currency, FieldMetadataType.Currency,
], ],
[DATE_AGGREGATE_OPERATIONS.earliest]: [
FieldMetadataType.DateTime,
FieldMetadataType.Date,
],
[DATE_AGGREGATE_OPERATIONS.latest]: [
FieldMetadataType.DateTime,
FieldMetadataType.Date,
],
}; };

View File

@ -6,11 +6,14 @@ import { RecordTableRecordGroupRows } from '@/object-record/record-table/compone
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider'; import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection'; import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType'; import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
export const RecordTableRecordGroupsBody = () => { export const RecordTableRecordGroupsBody = () => {
const allRecordIds = useRecoilComponentValueV2( const allRecordIds = useRecoilComponentValueV2(
@ -26,6 +29,10 @@ export const RecordTableRecordGroupsBody = () => {
ViewType.Table, ViewType.Table,
); );
const isAggregateQueryEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsAggregateQueryEnabled,
);
if (isRecordTableInitialLoading && allRecordIds.length === 0) { if (isRecordTableInitialLoading && allRecordIds.length === 0) {
return <RecordTableBodyLoading />; return <RecordTableBodyLoading />;
} }
@ -43,6 +50,12 @@ export const RecordTableRecordGroupsBody = () => {
<RecordTableRecordGroupSection /> <RecordTableRecordGroupSection />
<RecordTableRecordGroupRows /> <RecordTableRecordGroupRows />
</RecordTableBodyDroppable> </RecordTableBodyDroppable>
{isAggregateQueryEnabled && (
<RecordTableAggregateFooter
key={recordGroupId}
currentRecordGroupId={recordGroupId}
/>
)}
</RecordGroupContext.Provider> </RecordGroupContext.Provider>
</RecordTableRecordGroupBodyContextProvider> </RecordTableRecordGroupBodyContextProvider>
))} ))}

View File

@ -2,40 +2,119 @@ import styled from '@emotion/styled';
import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell'; import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell';
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { MOBILE_VIEWPORT } from 'twenty-ui';
const StyledTh = styled.th` const StyledTh = styled.th`
background-color: ${({ theme }) => theme.background.primary}; background-color: ${({ theme }) => theme.background.primary};
`; `;
const StyledTableFoot = styled.thead<{ endOfTableSticky?: boolean }>`
cursor: pointer;
th:nth-of-type(1) {
width: ${FIRST_TH_WIDTH};
left: 0;
border-right-color: ${({ theme }) => theme.background.primary};
}
th:nth-of-type(2) {
border-right-color: ${({ theme }) => theme.background.primary};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
}
&.first-columns-sticky {
th:nth-of-type(1) {
position: sticky;
left: 0;
z-index: 5;
transition: 0.3s ease;
}
th:nth-of-type(2) {
position: sticky;
left: 11px;
z-index: 5;
transition: 0.3s ease;
}
th:nth-of-type(3) {
position: sticky;
left: 43px;
z-index: 5;
transition: 0.3s ease;
&::after {
content: '';
position: absolute;
top: -1px;
height: calc(100% + 2px);
width: 4px;
right: 0px;
box-shadow: ${({ theme }) => theme.boxShadow.light};
clip-path: inset(0px -4px 0px 0px);
}
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: 34px;
max-width: 34px;
}
}
}
tr {
position: sticky;
z-index: 5;
background: ${({ theme }) => theme.background.primary};
${({ endOfTableSticky }) =>
endOfTableSticky &&
`
bottom: 10px;
&::after {
content: '';
position: absolute;
bottom: -10px;
left: 0;
right: 0;
height: 10px;
background: inherit;
}
`}
}
`;
export const RecordTableAggregateFooter = ({ export const RecordTableAggregateFooter = ({
currentRecordGroupId, currentRecordGroupId,
endOfTableSticky,
}: { }: {
currentRecordGroupId?: string; currentRecordGroupId?: string;
endOfTableSticky?: boolean;
}) => { }) => {
const visibleTableColumns = useRecoilComponentValueV2( const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector, visibleTableColumnsComponentSelector,
); );
return ( return (
<tr> <StyledTableFoot
<StyledTh /> id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
<StyledTh /> data-select-disable
{visibleTableColumns.map((column, index) => ( endOfTableSticky={endOfTableSticky}
<RecordTableColumnAggregateFooterCellContext.Provider >
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`} <tr>
value={{ <StyledTh />
viewFieldId: column.viewFieldId || '', <StyledTh />
fieldMetadataId: column.fieldMetadataId, {visibleTableColumns.map((column, index) => {
}} return (
> <RecordTableColumnAggregateFooterCellContext.Provider
<RecordTableAggregateFooterCell key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
currentRecordGroupId={currentRecordGroupId} value={{
isFirstCell={index === 0} viewFieldId: column.viewFieldId || '',
/> fieldMetadataId: column.fieldMetadataId,
</RecordTableColumnAggregateFooterCellContext.Provider> }}
))} >
</tr> <RecordTableAggregateFooterCell
currentRecordGroupId={currentRecordGroupId}
isFirstCell={index === 0}
/>
</RecordTableColumnAggregateFooterCellContext.Provider>
);
})}
</tr>
</StyledTableFoot>
); );
}; };

View File

@ -43,6 +43,7 @@ const StyledColumnFooterCell = styled.th<{
*::-webkit-scrollbar { *::-webkit-scrollbar {
display: none; display: none;
} }
border-top: 1px solid ${({ theme }) => theme.border.color.light};
`; `;
const StyledColumnFootContainer = styled.div` const StyledColumnFootContainer = styled.div`

View File

@ -1,6 +1,6 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { RecordTableColumnAggregateFooterAggregateOperationMenuItems } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems'; 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 { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -14,7 +14,7 @@ export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({
aggregateOperations, aggregateOperations,
title, title,
}: { }: {
aggregateOperations: AGGREGATE_OPERATIONS[]; aggregateOperations: ExtendedAggregateOperations[];
title: string; title: string;
}) => { }) => {
const { dropdownId, resetContent } = useContext( const { dropdownId, resetContent } = useContext(

View File

@ -1,8 +1,7 @@
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; 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 { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ReactNode, useContext } from 'react'; import { ReactNode, useContext } from 'react';
import { IconCheck, isDefined, MenuItem } from 'twenty-ui'; import { IconCheck, isDefined, MenuItem } from 'twenty-ui';
@ -11,7 +10,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
aggregateOperations, aggregateOperations,
children, children,
}: { }: {
aggregateOperations: AGGREGATE_OPERATIONS[]; aggregateOperations: ExtendedAggregateOperations[];
children?: ReactNode; children?: ReactNode;
}) => { }) => {
const { const {
@ -19,7 +18,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
currentViewFieldAggregateOperation, currentViewFieldAggregateOperation,
} = useViewFieldAggregateOperation(); } = useViewFieldAggregateOperation();
const { dropdownId, resetContent, fieldMetadataType } = useContext( const { dropdownId, resetContent } = useContext(
RecordTableColumnAggregateFooterDropdownContext, RecordTableColumnAggregateFooterDropdownContext,
); );
const { closeDropdown } = useDropdown(dropdownId); const { closeDropdown } = useDropdown(dropdownId);
@ -32,12 +31,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({
updateViewFieldAggregateOperation(operation); updateViewFieldAggregateOperation(operation);
closeDropdown(); closeDropdown();
}} }}
text={getAggregateOperationLabel( text={getAggregateOperationLabel(operation)}
convertAggregateOperationToExtendedAggregateOperation(
operation,
fieldMetadataType,
),
)}
RightIcon={ RightIcon={
currentViewFieldAggregateOperation === operation currentViewFieldAggregateOperation === operation
? IconCheck ? IconCheck

View File

@ -1,9 +1,12 @@
import { useDropdown } from '@/dropdown/hooks/useDropdown'; import { useDropdown } from '@/dropdown/hooks/useDropdown';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
import { RecordTableColumnAggregateFooterDropdownSubmenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent'; 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 { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { RecordTableColumnAggregateFooterMenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent'; 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 { 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 { DATE_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/dateAggregateOperationOptions';
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions'; 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 { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
@ -21,7 +24,9 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => {
case 'moreAggregateOperationOptions': { case 'moreAggregateOperationOptions': {
const aggregateOperations = availableAggregateOperations.filter( const aggregateOperations = availableAggregateOperations.filter(
(aggregateOperation) => (aggregateOperation) =>
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), !STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(
aggregateOperation as AGGREGATE_OPERATIONS,
),
); );
return ( return (
@ -34,7 +39,9 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => {
case 'countAggregateOperationsOptions': { case 'countAggregateOperationsOptions': {
const aggregateOperations = availableAggregateOperations.filter( const aggregateOperations = availableAggregateOperations.filter(
(aggregateOperation) => (aggregateOperation) =>
COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), COUNT_AGGREGATE_OPERATION_OPTIONS.includes(
aggregateOperation as AGGREGATE_OPERATIONS,
),
); );
return ( return (
<RecordTableColumnAggregateFooterDropdownSubmenuContent <RecordTableColumnAggregateFooterDropdownSubmenuContent
@ -46,7 +53,9 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => {
case 'percentAggregateOperationsOptions': { case 'percentAggregateOperationsOptions': {
const aggregateOperations = availableAggregateOperations.filter( const aggregateOperations = availableAggregateOperations.filter(
(aggregateOperation) => (aggregateOperation) =>
PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(
aggregateOperation as AGGREGATE_OPERATIONS,
),
); );
return ( return (
<RecordTableColumnAggregateFooterDropdownSubmenuContent <RecordTableColumnAggregateFooterDropdownSubmenuContent
@ -55,6 +64,20 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => {
/> />
); );
} }
case 'datesAggregateOperationsOptions': {
const aggregateOperations = availableAggregateOperations.filter(
(aggregateOperation) =>
DATE_AGGREGATE_OPERATION_OPTIONS.includes(
aggregateOperation as DATE_AGGREGATE_OPERATIONS,
),
);
return (
<RecordTableColumnAggregateFooterDropdownSubmenuContent
aggregateOperations={aggregateOperations}
title="Dates"
/>
);
}
default: default:
return <RecordTableColumnAggregateFooterMenuContent />; return <RecordTableColumnAggregateFooterMenuContent />;
} }

View File

@ -1,6 +1,8 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; 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 { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
import { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType'; import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -8,13 +10,18 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { MenuItem } from 'twenty-ui'; import { isFieldMetadataDateKind } from 'twenty-shared';
import { IconCheck, isDefined, MenuItem } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
export const RecordTableColumnAggregateFooterMenuContent = () => { export const RecordTableColumnAggregateFooterMenuContent = () => {
const { fieldMetadataId, dropdownId, onContentChange } = useContext( const {
RecordTableColumnAggregateFooterDropdownContext, fieldMetadataId,
); dropdownId,
onContentChange,
fieldMetadataType,
resetContent,
} = useContext(RecordTableColumnAggregateFooterDropdownContext);
const { closeDropdown } = useDropdown(dropdownId); const { closeDropdown } = useDropdown(dropdownId);
const { objectMetadataItem } = useRecordTableContextOrThrow(); const { objectMetadataItem } = useRecordTableContextOrThrow();
@ -36,16 +43,24 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
[fieldMetadataId, objectMetadataItem.fields], [fieldMetadataId, objectMetadataItem.fields],
); );
const fieldIsDateKind = isFieldMetadataDateKind(fieldMetadataType);
const nonStandardAvailableAggregateOperation = const nonStandardAvailableAggregateOperation =
availableAggregateOperation.filter( availableAggregateOperation.filter((aggregateOperation) =>
(aggregateOperation) => NON_STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(
!STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), aggregateOperation as AGGREGATE_OPERATIONS,
),
); );
const fieldIsRelation = const fieldIsRelation =
objectMetadataItem.fields.find((field) => field.id === fieldMetadataId) objectMetadataItem.fields.find((field) => field.id === fieldMetadataId)
?.type === FieldMetadataType.Relation; ?.type === FieldMetadataType.Relation;
const {
updateViewFieldAggregateOperation,
currentViewFieldAggregateOperation,
} = useViewFieldAggregateOperation();
return ( return (
<> <>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
@ -65,6 +80,15 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
hasSubMenu hasSubMenu
/> />
)} )}
{fieldIsDateKind && (
<MenuItem
onClick={() => {
onContentChange('datesAggregateOperationsOptions');
}}
text={'Dates'}
hasSubMenu
/>
)}
{nonStandardAvailableAggregateOperation.length > 0 ? ( {nonStandardAvailableAggregateOperation.length > 0 ? (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
@ -74,6 +98,21 @@ export const RecordTableColumnAggregateFooterMenuContent = () => {
hasSubMenu hasSubMenu
/> />
) : null} ) : null}
<MenuItem
key={'none'}
onClick={() => {
updateViewFieldAggregateOperation(null);
resetContent();
closeDropdown();
}}
text={'None'}
RightIcon={
!isDefined(currentViewFieldAggregateOperation)
? IconCheck
: undefined
}
aria-selected={!isDefined(currentViewFieldAggregateOperation)}
/>
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</> </>
); );

View File

@ -36,8 +36,9 @@ const StyledValueContainer = styled(StyledScrollableContainer)`
padding: 0 8px; padding: 0 8px;
`; `;
const StyledValue = styled(StyledScrollableContainer)` const StyledValue = styled.div`
color: ${({ theme }) => theme.color.gray60}; color: ${({ theme }) => theme.font.color.primary};
max-width: 100%;
`; `;
export const RecordTableColumnAggregateFooterValue = ({ export const RecordTableColumnAggregateFooterValue = ({

View File

@ -76,7 +76,9 @@ export const RecordTableColumnAggregateFooterValueCell = ({
fieldMetadataId={fieldMetadataId} fieldMetadataId={fieldMetadataId}
dropdownId={dropdownId} dropdownId={dropdownId}
/> />
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} /> {!hasAggregateOperationForViewField && (
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
)}
</> </>
) : ( ) : (
<></> <></>

View File

@ -0,0 +1,6 @@
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
export const DATE_AGGREGATE_OPERATION_OPTIONS = [
DATE_AGGREGATE_OPERATIONS.earliest,
DATE_AGGREGATE_OPERATIONS.latest,
];

View File

@ -1,5 +1,5 @@
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions'; 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 { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
export const STANDARD_AGGREGATE_OPERATION_OPTIONS = [ export const STANDARD_AGGREGATE_OPERATION_OPTIONS = [
...COUNT_AGGREGATE_OPERATION_OPTIONS, ...COUNT_AGGREGATE_OPERATION_OPTIONS,

View File

@ -1,6 +1,7 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords'; import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useContext } from 'react'; import { useContext } from 'react';
@ -18,13 +19,19 @@ export const useViewFieldAggregateOperation = () => {
); );
const { updateViewFieldRecords } = usePersistViewFieldRecords(); const { updateViewFieldRecords } = usePersistViewFieldRecords();
const updateViewFieldAggregateOperation = ( const updateViewFieldAggregateOperation = (
aggregateOperation: AGGREGATE_OPERATIONS | null, aggregateOperation: ExtendedAggregateOperations | null,
) => { ) => {
if (!currentViewField) { if (!currentViewField) {
throw new Error('ViewField not found'); throw new Error('ViewField not found');
} }
updateViewFieldRecords([ updateViewFieldRecords([
{ ...currentViewField, aggregateOperation: aggregateOperation }, {
...currentViewField,
aggregateOperation:
convertExtendedAggregateOperationToAggregateOperation(
aggregateOperation,
),
},
]); ]);
}; };

View File

@ -1,8 +1,8 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
export const viewFieldAggregateOperationState = createFamilyState< export const viewFieldAggregateOperationState = createFamilyState<
AGGREGATE_OPERATIONS | null | undefined, ExtendedAggregateOperations | null | undefined,
{ viewFieldId: string } { viewFieldId: string }
>({ >({
key: 'viewFieldAggregateOperationState', key: 'viewFieldAggregateOperationState',

View File

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

View File

@ -1,5 +1,6 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation'; import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations'; import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation'; import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
@ -14,7 +15,7 @@ export const getAvailableAggregateOperationsForFieldMetadataType = ({
return [AGGREGATE_OPERATIONS.count]; return [AGGREGATE_OPERATIONS.count];
} }
const availableAggregateOperations = new Set<AGGREGATE_OPERATIONS>([ const availableAggregateOperations = new Set<ExtendedAggregateOperations>([
AGGREGATE_OPERATIONS.count, AGGREGATE_OPERATIONS.count,
AGGREGATE_OPERATIONS.countEmpty, AGGREGATE_OPERATIONS.countEmpty,
AGGREGATE_OPERATIONS.countNotEmpty, AGGREGATE_OPERATIONS.countNotEmpty,
@ -35,7 +36,9 @@ export const getAvailableAggregateOperationsForFieldMetadataType = ({
), ),
) )
.forEach((operation) => .forEach((operation) =>
availableAggregateOperations.add(operation as AGGREGATE_OPERATIONS), availableAggregateOperations.add(
operation as ExtendedAggregateOperations,
),
); );
return Array.from(availableAggregateOperations); return Array.from(availableAggregateOperations);

View File

@ -1,6 +1,6 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations';
export type ExtendedAggregateOperations = export type ExtendedAggregateOperations =
| AGGREGATE_OPERATIONS | AGGREGATE_OPERATIONS
| 'EARLIEST' | DATE_AGGREGATE_OPERATIONS;
| 'LATEST';

View File

@ -2,7 +2,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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 { 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 { 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 { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields'; import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';

View File

@ -1,7 +1,7 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation'; import { FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION } from '@/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation';
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions'; 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 { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations'; import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations';
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap'; import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';

View File

@ -1,4 +1,5 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { isFieldMetadataDateKind } from 'twenty-shared'; import { isFieldMetadataDateKind } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -9,10 +10,10 @@ export const convertAggregateOperationToExtendedAggregateOperation = (
): ExtendedAggregateOperations => { ): ExtendedAggregateOperations => {
if (isFieldMetadataDateKind(fieldType) === true) { if (isFieldMetadataDateKind(fieldType) === true) {
if (aggregateOperation === AGGREGATE_OPERATIONS.min) { if (aggregateOperation === AGGREGATE_OPERATIONS.min) {
return 'EARLIEST'; return DATE_AGGREGATE_OPERATIONS.earliest;
} }
if (aggregateOperation === AGGREGATE_OPERATIONS.max) { if (aggregateOperation === AGGREGATE_OPERATIONS.max) {
return 'LATEST'; return DATE_AGGREGATE_OPERATIONS.latest;
} }
} }
return aggregateOperation; return aggregateOperation;

View File

@ -1,14 +1,15 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
export const convertExtendedAggregateOperationToAggregateOperation = ( export const convertExtendedAggregateOperationToAggregateOperation = (
extendedAggregateOperation: ExtendedAggregateOperations, extendedAggregateOperation: ExtendedAggregateOperations | null,
) => { ) => {
if (extendedAggregateOperation === 'EARLIEST') { if (extendedAggregateOperation === DATE_AGGREGATE_OPERATIONS.earliest) {
return AGGREGATE_OPERATIONS.min; return AGGREGATE_OPERATIONS.min;
} }
if (extendedAggregateOperation === 'LATEST') { if (extendedAggregateOperation === DATE_AGGREGATE_OPERATIONS.latest) {
return AGGREGATE_OPERATIONS.max; return AGGREGATE_OPERATIONS.max;
} }
return extendedAggregateOperation; return extendedAggregateOperation;

View File

@ -1,10 +1,12 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; 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';
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared'; import { capitalize, isFieldMetadataDateKind } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
type NameForAggregation = { type NameForAggregation = {
[T in AGGREGATE_OPERATIONS]?: string; [T in ExtendedAggregateOperations]?: string;
}; };
type Aggregations = { type Aggregations = {
@ -58,8 +60,8 @@ export const getAvailableAggregationsFromObjectFields = (
if (isFieldMetadataDateKind(field.type) === true) { if (isFieldMetadataDateKind(field.type) === true) {
acc[field.name] = { acc[field.name] = {
...acc[field.name], ...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`, [DATE_AGGREGATE_OPERATIONS.earliest]: `min${capitalize(field.name)}`,
[AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`, [DATE_AGGREGATE_OPERATIONS.latest]: `max${capitalize(field.name)}`,
}; };
} }

View File

@ -1,15 +1,13 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation'; import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields'; import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap'; import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const getAvailableFieldsIdsForAggregationFromObjectFields = ( export const getAvailableFieldsIdsForAggregationFromObjectFields = (
fields: FieldMetadataItem[], fields: FieldMetadataItem[],
targetAggregateOperations: AGGREGATE_OPERATIONS[], targetAggregateOperations: ExtendedAggregateOperations[],
): AvailableFieldsForAggregateOperation => { ): AvailableFieldsForAggregateOperation => {
const aggregationMap = initializeAvailableFieldsForAggregateOperationMap( const aggregationMap = initializeAvailableFieldsForAggregateOperationMap(
targetAggregateOperations, targetAggregateOperations,
@ -20,20 +18,12 @@ export const getAvailableFieldsIdsForAggregationFromObjectFields = (
return fields.reduce((acc, field) => { return fields.reduce((acc, field) => {
if (isDefined(allAggregations[field.name])) { if (isDefined(allAggregations[field.name])) {
Object.keys(allAggregations[field.name]).forEach((aggregation) => { Object.keys(allAggregations[field.name]).forEach((aggregation) => {
if ( const typedAggregation = aggregation as ExtendedAggregateOperations;
targetAggregateOperations.includes( if (targetAggregateOperations.includes(typedAggregation)) {
aggregation as AGGREGATE_OPERATIONS, if (!isDefined(acc[typedAggregation])) {
) acc[typedAggregation] = [];
) {
const convertedAggregateOperation: ExtendedAggregateOperations =
convertAggregateOperationToExtendedAggregateOperation(
aggregation as AGGREGATE_OPERATIONS,
field.type,
);
if (!isDefined(acc[convertedAggregateOperation])) {
acc[convertedAggregateOperation] = [];
} }
(acc[convertedAggregateOperation] as string[]).push(field.id); (acc[typedAggregation] as string[]).push(field.id);
} }
}); });
} }

View File

@ -1,14 +1,13 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation'; import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
export const initializeAvailableFieldsForAggregateOperationMap = ( export const initializeAvailableFieldsForAggregateOperationMap = (
aggregateOperations: AGGREGATE_OPERATIONS[], aggregateOperations: ExtendedAggregateOperations[],
): AvailableFieldsForAggregateOperation => { ): AvailableFieldsForAggregateOperation => {
return aggregateOperations.reduce( return aggregateOperations.reduce(
(acc, operation) => ({ (acc, operation) => ({
...acc, ...acc,
[convertAggregateOperationToExtendedAggregateOperation(operation)]: [], [operation]: [],
}), }),
{}, {},
); );

View File

@ -1,30 +1,28 @@
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownOnToggleEffect } from '@/ui/layout/dropdown/components/DropdownOnToggleEffect';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
import { import {
Placement,
autoUpdate, autoUpdate,
flip, flip,
offset, offset,
Placement,
size, size,
useFloating, useFloating,
} from '@floating-ui/react'; } from '@floating-ui/react';
import { MouseEvent, ReactNode } from 'react'; import { MouseEvent, ReactNode } from 'react';
import { Keys } from 'react-hotkeys-hook';
import { useDropdown } from '../hooks/useDropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext';
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
import { Keys } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
import { sleep } from '~/utils/sleep'; import { sleep } from '~/utils/sleep';
import { DropdownOnToggleEffect } from './DropdownOnToggleEffect'; import { useDropdown } from '../hooks/useDropdown';
const StyledDropdownFallbackAnchor = styled.div` const StyledDropdownFallbackAnchor = styled.div`
left: 0; left: 0;
@ -32,6 +30,26 @@ const StyledDropdownFallbackAnchor = styled.div`
top: 0; top: 0;
`; `;
type StyledHeaderDivProps = {
isUnfolded?: boolean;
isActive?: boolean;
};
const StyledHeaderDiv = styled.div<StyledHeaderDivProps>`
& button,
& > * {
background: ${({ theme, isUnfolded }) =>
isUnfolded ? theme.background.transparent.light : 'none'};
&:hover {
background: ${({ theme, isUnfolded }) =>
isUnfolded
? theme.background.transparent.medium
: theme.background.transparent.light};
}
}
`;
type DropdownProps = { type DropdownProps = {
className?: string; className?: string;
clickableComponent?: ReactNode; clickableComponent?: ReactNode;
@ -133,16 +151,17 @@ export const Dropdown = ({
<DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}> <DropdownScope dropdownScopeId={getScopeIdFromComponentId(dropdownId)}>
<> <>
{isDefined(clickableComponent) ? ( {isDefined(clickableComponent) ? (
<div <StyledHeaderDiv
ref={refs.setReference} ref={refs.setReference}
onClick={handleClickableComponentClick} onClick={handleClickableComponentClick}
aria-controls={`${dropdownId}-options`} aria-controls={`${dropdownId}-options`}
aria-expanded={isDropdownOpen} aria-expanded={isDropdownOpen}
aria-haspopup={true} aria-haspopup={true}
role="button" role="button"
isUnfolded={isDropdownOpen}
> >
{clickableComponent} {clickableComponent}
</div> </StyledHeaderDiv>
) : ( ) : (
<StyledDropdownFallbackAnchor ref={refs.setReference} /> <StyledDropdownFallbackAnchor ref={refs.setReference} />
)} )}

View File

@ -8,8 +8,6 @@ type StyledDropdownButtonProps = {
export const StyledHeaderDropdownButton = styled.button<StyledDropdownButtonProps>` export const StyledHeaderDropdownButton = styled.button<StyledDropdownButtonProps>`
font-family: inherit; font-family: inherit;
align-items: center; align-items: center;
background: ${({ theme, isUnfolded }) =>
isUnfolded ? theme.background.transparent.light : theme.background.primary};
border: none; border: none;
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ isActive, theme }) => color: ${({ isActive, theme }) =>
@ -22,11 +20,4 @@ export const StyledHeaderDropdownButton = styled.button<StyledDropdownButtonProp
padding-right: ${({ theme }) => theme.spacing(2)}; padding-right: ${({ theme }) => theme.spacing(2)};
user-select: none; user-select: none;
&:hover {
background: ${({ theme, isUnfolded }) =>
isUnfolded
? theme.background.transparent.medium
: theme.background.transparent.light};
}
`; `;

View File

@ -1,4 +1,5 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useUpdateView } from '@/views/hooks/useUpdateView'; import { useUpdateView } from '@/views/hooks/useUpdateView';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
@ -13,13 +14,18 @@ export const useUpdateViewAggregate = () => {
kanbanAggregateOperation, kanbanAggregateOperation,
}: { }: {
kanbanAggregateOperationFieldMetadataId: string | null; kanbanAggregateOperationFieldMetadataId: string | null;
kanbanAggregateOperation: AGGREGATE_OPERATIONS | null; kanbanAggregateOperation: ExtendedAggregateOperations | null;
}) => }) => {
const convertedKanbanAggregateOperation =
convertExtendedAggregateOperationToAggregateOperation(
kanbanAggregateOperation,
);
updateView({ updateView({
id: currentViewId, id: currentViewId,
kanbanAggregateOperationFieldMetadataId, kanbanAggregateOperationFieldMetadataId,
kanbanAggregateOperation, kanbanAggregateOperation: convertedKanbanAggregateOperation,
}), });
},
[currentViewId, updateView], [currentViewId, updateView],
); );

View File

@ -1,7 +1,7 @@
import { FieldMetadataType } from 'src/types/FieldMetadataType'; import { FieldMetadataType } from 'src/types/FieldMetadataType';
export const isFieldMetadataDateKind = ( export const isFieldMetadataDateKind = (
fieldMetadataType: FieldMetadataType, fieldMetadataType?: FieldMetadataType,
): fieldMetadataType is ): fieldMetadataType is
| FieldMetadataType.DATE | FieldMetadataType.DATE
| FieldMetadataType.DATE_TIME => { | FieldMetadataType.DATE_TIME => {