From 2e0169b954851dd9b09ce75233c0075f599f90df Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:01:36 +0100 Subject: [PATCH] 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 --- .../graphql/types/RecordGqlFieldsAggregate.ts | 7 +- .../hooks/useAggregateRecords.ts | 4 +- .../hooks/useAggregateRecordsQuery.ts | 4 +- .../components/MultipleFiltersButton.tsx | 9 +- .../components/ObjectSortDropdownButton.tsx | 8 +- ...rdColumnHeaderAggregateDropdownContent.tsx | 23 +++- ...mnHeaderAggregateDropdownFieldsContent.tsx | 8 +- ...lumnHeaderAggregateDropdownMenuContent.tsx | 7 ++ .../computeAggregateValueAndLabel.test.ts | 17 +-- .../getAggregateOperationLabel.test.ts | 9 +- .../utils/computeAggregateValueAndLabel.ts | 27 ++-- .../utils/getAggregateOperationLabel.ts | 5 +- ...cordBoardColumnHeaderAggregateContentId.ts | 1 + .../components/RecordIndexContainer.tsx | 35 +++++- .../RecordIndexTableContainerEffect.tsx | 25 +++- ...ecordIndexKanbanAggregateOperationState.ts | 4 +- .../record-table/components/RecordTable.tsx | 4 +- .../components/RecordTableRecordGroupRows.tsx | 13 -- .../constants/DateAggregateOperations.ts | 4 + ...ailableForNonStandardAggregateOperation.ts | 13 +- .../RecordTableRecordGroupsBody.tsx | 13 ++ .../components/RecordTableAggregateFooter.tsx | 115 +++++++++++++++--- .../RecordTableAggregateFooterCell.tsx | 1 + ...eColumnAggregateDropdownSubmenuContent.tsx | 4 +- ...egateFooterAggregateOperationMenuItems.tsx | 14 +-- ...leColumnAggregateFooterDropdownContent.tsx | 31 ++++- ...dTableColumnAggregateFooterMenuContent.tsx | 55 +++++++-- .../RecordTableColumnAggregateFooterValue.tsx | 5 +- ...ordTableColumnAggregateFooterValueCell.tsx | 4 +- .../dateAggregateOperationOptions.tsx | 6 + ...x => percentAggregateOperationOptions.tsx} | 0 .../standardAggregateOperationOptions.tsx | 2 +- .../hooks/useViewFieldAggregateOperation.tsx | 13 +- .../viewFieldAggregateOperationState.ts | 4 +- .../RecordTableFooterAggregateContentId.tsx | 3 +- ...AggregateOperationsForFieldMetadataType.ts | 7 +- .../types/ExtendedAggregateOperations.ts | 4 +- ...sIdsForAggregationFromObjectFields.test.ts | 2 +- ...ableFieldsForAggregateOperationMap.test.ts | 2 +- ...teOperationToExtendedAggregateOperation.ts | 5 +- ...dAggregateOperationToAggregateOperation.ts | 7 +- ...etAvailableAggregationsFromObjectFields.ts | 8 +- ...FieldsIdsForAggregationFromObjectFields.ts | 22 +--- ...AvailableFieldsForAggregateOperationMap.ts | 7 +- .../layout/dropdown/components/Dropdown.tsx | 47 ++++--- .../components/StyledHeaderDropdownButton.tsx | 9 -- .../views/hooks/useUpdateViewAggregate.ts | 16 ++- .../fieldMetadata/isFieldMetadataDateKind.ts | 2 +- 48 files changed, 440 insertions(+), 195 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/constants/DateAggregateOperations.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/dateAggregateOperationOptions.tsx rename packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/{percentAggregateOperationOption.tsx => percentAggregateOperationOptions.tsx} (100%) diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts index 990a73181..71f36ee4d 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts @@ -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; +export type RecordGqlFieldsAggregate = Record< + string, + ExtendedAggregateOperations[] +>; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts index 2a3b63578..19262db75 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts @@ -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; }; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts index ea87546fe..3333ae281 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts @@ -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'; @@ -10,7 +10,7 @@ import { isDefined } from 'twenty-ui'; export type GqlFieldToFieldMap = { [gqlField: string]: [ fieldName: string, - aggregateOperation: AGGREGATE_OPERATIONS, + aggregateOperation: ExtendedAggregateOperations, ]; }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersButton.tsx index 5306d3681..e13d55d55 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersButton.tsx @@ -6,9 +6,7 @@ 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(); @@ -16,10 +14,7 @@ export const MultipleFiltersButton = () => { }; return ( - + Filter ); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx index 014905a5e..39c0e312d 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx @@ -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'; @@ -78,8 +77,6 @@ export const ObjectSortDropdownButton = ({ const { recordIndexId } = useRecordIndexContextOrThrow(); - const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID); - const handleButtonClick = () => { toggleSortDropdown(); }; @@ -141,10 +138,7 @@ export const ObjectSortDropdownButton = ({ dropdownHotkeyScope={hotkeyScope} dropdownOffset={{ y: 8 }} clickableComponent={ - + Sort } diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx index c461f846d..dc7aa8844 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx @@ -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'; @@ -41,15 +42,31 @@ export const AggregateDropdownContent = () => { /> ); } + case 'datesAggregateOperationOptions': { + const datesAvailableAggregations: AvailableFieldsForAggregateOperation = + getAvailableFieldsIdsForAggregationFromObjectFields( + objectMetadataItem.fields, + [ + DATE_AGGREGATE_OPERATIONS.earliest, + DATE_AGGREGATE_OPERATIONS.latest, + ], + ); + return ( + + ); + } case 'moreAggregateOperationOptions': { - const availableAggregations: AvailableFieldsForAggregateOperation = + const availableAggregationsWithoutDates: AvailableFieldsForAggregateOperation = getAvailableFieldsIdsForAggregationFromObjectFields( objectMetadataItem.fields, NON_STANDARD_AGGREGATE_OPERATION_OPTIONS, ); return ( ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx index def721618..2f4843e68 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx @@ -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'; @@ -47,9 +46,6 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { ); if (!isDefined(aggregateOperation)) return <>; - - const convertedAggregateOperation = - convertExtendedAggregateOperationToAggregateOperation(aggregateOperation); return ( <> { onClick={() => { updateViewAggregate({ kanbanAggregateOperationFieldMetadataId: fieldId, - kanbanAggregateOperation: convertedAggregateOperation, + kanbanAggregateOperation: aggregateOperation, }); closeDropdown(); }} @@ -85,7 +81,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { recordIndexKanbanAggregateOperation?.fieldMetadataId === fieldId && recordIndexKanbanAggregateOperation?.operation === - convertedAggregateOperation + aggregateOperation ? IconCheck : undefined } diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx index 558282439..e2eda18d7 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx @@ -42,6 +42,13 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => { text={'Percent'} hasSubMenu /> + { + onContentChange('datesAggregateOperationOptions'); + }} + text={'Dates'} + hasSubMenu + /> { onContentChange('moreAggregateOperationOptions'); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts index 1b588ab70..fab7afaa2 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts @@ -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'; @@ -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, }); @@ -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, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/getAggregateOperationLabel.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/getAggregateOperationLabel.test.ts index 0893212f6..a5ab5c92b 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/getAggregateOperationLabel.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/getAggregateOperationLabel.test.ts @@ -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', () => { @@ -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', diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts index 631e9f364..c348e3532 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts @@ -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'; @@ -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; @@ -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, + ) + ) { 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) { @@ -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, diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts index 74f492e86..aa1cc8ba3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts @@ -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 = ( @@ -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}`); diff --git a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts b/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts index 0de0a4ba4..d342eade6 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts @@ -3,4 +3,5 @@ export type RecordBoardColumnHeaderAggregateContentId = | 'aggregateFields' | 'countAggregateOperationsOptions' | 'percentAggregateOperationsOptions' + | 'datesAggregateOperationOptions' | 'moreAggregateOperationOptions'; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index ddecea43b..0ebff1827 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; 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 { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; 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 { ViewBar } from '@/views/components/ViewBar'; import { ViewField } from '@/views/types/ViewField'; @@ -125,6 +127,9 @@ export const RecordIndexContainer = () => { } for (const viewField of viewFields) { + const viewFieldMetadataType = objectMetadataItem.fields?.find( + (field) => field.id === viewField.fieldMetadataId, + )?.type; const aggregateOperationForViewField = snapshot .getLoadable( viewFieldAggregateOperationState({ @@ -133,17 +138,29 @@ export const RecordIndexContainer = () => { ) .getValue(); - if (aggregateOperationForViewField !== viewField.aggregateOperation) { + const convertedViewFieldAggregateOperation = isDefined( + viewField.aggregateOperation, + ) + ? convertAggregateOperationToExtendedAggregateOperation( + viewField.aggregateOperation, + viewFieldMetadataType, + ) + : viewField.aggregateOperation; + + if ( + aggregateOperationForViewField !== + convertedViewFieldAggregateOperation + ) { set( viewFieldAggregateOperationState({ viewFieldId: viewField.id, }), - viewField.aggregateOperation, + convertedViewFieldAggregateOperation, ); } } }, - [columnDefinitions, setTableColumns], + [columnDefinitions, objectMetadataItem.fields, setTableColumns], ); const onViewGroupsChange = useCallback( @@ -220,8 +237,18 @@ export const RecordIndexContainer = () => { setRecordIndexViewKanbanFieldMetadataIdState( view.kanbanFieldMetadataId, ); + const kanbanAggregateOperationFieldMetadataType = + objectMetadataItem.fields?.find( + (field) => + field.id === view.kanbanAggregateOperationFieldMetadataId, + )?.type; setRecordIndexViewKanbanAggregateOperationState({ - operation: view.kanbanAggregateOperation, + operation: isDefined(view.kanbanAggregateOperation) + ? convertAggregateOperationToExtendedAggregateOperation( + view.kanbanAggregateOperation, + kanbanAggregateOperationFieldMetadataType, + ) + : view.kanbanAggregateOperation, fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId, }); setRecordIndexIsCompactModeActive(view.isCompact); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index a4b212477..040d6ae35 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -8,9 +8,11 @@ import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/us import { useSetRecordIndexEntityCount } from '@/object-record/record-index/hooks/useSetRecordIndexEntityCount'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; 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 { ViewField } from '@/views/types/ViewField'; import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-ui'; export const RecordIndexTableContainerEffect = () => { const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow(); @@ -83,16 +85,33 @@ export const RecordIndexTableContainerEffect = () => { ) .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( viewFieldAggregateOperationState({ viewFieldId: viewField.id, }), - viewField.aggregateOperation, + convertedViewFieldAggregateOperation, ); } }, - [], + [columnDefinitions], ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexKanbanAggregateOperationState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexKanbanAggregateOperationState.ts index 664a6755c..c0ba9cf3c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexKanbanAggregateOperationState.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexKanbanAggregateOperationState.ts @@ -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'; export type KanbanAggregateOperation = { - operation?: AGGREGATE_OPERATIONS | null; + operation?: ExtendedAggregateOperations | null; fieldMetadataId?: string | null; } | null; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index b23c08ca3..571510fcb 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -100,7 +100,9 @@ export const RecordTable = () => { {isAggregateQueryEnabled && !hasRecordGroups && !isRecordTableInitialLoading && - allRecordIds.length > 0 && } + allRecordIds.length > 0 && ( + + )} { - const isAggregateQueryEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsAggregateQueryEnabled, - ); - const currentRecordGroupId = useCurrentRecordGroupId(); const allRecordIds = useRecoilComponentValueV2( @@ -66,12 +59,6 @@ export const RecordTableRecordGroupRows = () => { - {isAggregateQueryEnabled && ( - - )} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/DateAggregateOperations.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/DateAggregateOperations.ts new file mode 100644 index 000000000..55d92a059 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/DateAggregateOperations.ts @@ -0,0 +1,4 @@ +export enum DATE_AGGREGATE_OPERATIONS { + earliest = 'EARLIEST', + latest = 'LATEST', +} diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts index c2fea0db1..4903eeaad 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts @@ -1,18 +1,15 @@ 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'; export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = { [AGGREGATE_OPERATIONS.min]: [ FieldMetadataType.Number, FieldMetadataType.Currency, - FieldMetadataType.DateTime, - FieldMetadataType.Date, ], [AGGREGATE_OPERATIONS.max]: [ FieldMetadataType.Number, FieldMetadataType.Currency, - FieldMetadataType.DateTime, - FieldMetadataType.Date, ], [AGGREGATE_OPERATIONS.avg]: [ FieldMetadataType.Number, @@ -22,4 +19,12 @@ export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = { FieldMetadataType.Number, FieldMetadataType.Currency, ], + [DATE_AGGREGATE_OPERATIONS.earliest]: [ + FieldMetadataType.DateTime, + FieldMetadataType.Date, + ], + [DATE_AGGREGATE_OPERATIONS.latest]: [ + FieldMetadataType.DateTime, + FieldMetadataType.Date, + ], }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx index 087019240..0b735ebd2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx @@ -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 { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; 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 { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { FeatureFlagKey } from '~/generated/graphql'; export const RecordTableRecordGroupsBody = () => { const allRecordIds = useRecoilComponentValueV2( @@ -26,6 +29,10 @@ export const RecordTableRecordGroupsBody = () => { ViewType.Table, ); + const isAggregateQueryEnabled = useIsFeatureEnabled( + FeatureFlagKey.IsAggregateQueryEnabled, + ); + if (isRecordTableInitialLoading && allRecordIds.length === 0) { return ; } @@ -43,6 +50,12 @@ export const RecordTableRecordGroupsBody = () => { + {isAggregateQueryEnabled && ( + + )} ))} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx index 1ddda7388..cba9d7125 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx @@ -2,40 +2,119 @@ import styled from '@emotion/styled'; 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 { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { MOBILE_VIEWPORT } from 'twenty-ui'; const StyledTh = styled.th` 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 = ({ currentRecordGroupId, + endOfTableSticky, }: { currentRecordGroupId?: string; + + endOfTableSticky?: boolean; }) => { const visibleTableColumns = useRecoilComponentValueV2( visibleTableColumnsComponentSelector, ); return ( - - - - {visibleTableColumns.map((column, index) => ( - - - - ))} - + + + + + {visibleTableColumns.map((column, index) => { + return ( + + + + ); + })} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx index febc995d6..04b9a51d6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx @@ -43,6 +43,7 @@ const StyledColumnFooterCell = styled.th<{ *::-webkit-scrollbar { display: none; } + border-top: 1px solid ${({ theme }) => theme.border.color.light}; `; const StyledColumnFootContainer = styled.div` diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent.tsx index d394ba3a8..cc713891a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent.tsx @@ -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 { 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 { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -14,7 +14,7 @@ export const RecordTableColumnAggregateFooterDropdownSubmenuContent = ({ aggregateOperations, title, }: { - aggregateOperations: AGGREGATE_OPERATIONS[]; + aggregateOperations: ExtendedAggregateOperations[]; title: string; }) => { const { dropdownId, resetContent } = useContext( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems.tsx index 75f51312d..6e852d10c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterAggregateOperationMenuItems.tsx @@ -1,8 +1,7 @@ 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 { 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 { ReactNode, useContext } from 'react'; import { IconCheck, isDefined, MenuItem } from 'twenty-ui'; @@ -11,7 +10,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({ aggregateOperations, children, }: { - aggregateOperations: AGGREGATE_OPERATIONS[]; + aggregateOperations: ExtendedAggregateOperations[]; children?: ReactNode; }) => { const { @@ -19,7 +18,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({ currentViewFieldAggregateOperation, } = useViewFieldAggregateOperation(); - const { dropdownId, resetContent, fieldMetadataType } = useContext( + const { dropdownId, resetContent } = useContext( RecordTableColumnAggregateFooterDropdownContext, ); const { closeDropdown } = useDropdown(dropdownId); @@ -32,12 +31,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({ updateViewFieldAggregateOperation(operation); closeDropdown(); }} - text={getAggregateOperationLabel( - convertAggregateOperationToExtendedAggregateOperation( - operation, - fieldMetadataType, - ), - )} + text={getAggregateOperationLabel(operation)} RightIcon={ currentViewFieldAggregateOperation === operation ? IconCheck diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent.tsx index c362d11d5..e0f543e8d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent.tsx @@ -1,9 +1,12 @@ 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 { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; import { RecordTableColumnAggregateFooterMenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent'; import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions'; -import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption'; +import { 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 { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType'; @@ -21,7 +24,9 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => { case 'moreAggregateOperationOptions': { const aggregateOperations = availableAggregateOperations.filter( (aggregateOperation) => - !STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), + !STANDARD_AGGREGATE_OPERATION_OPTIONS.includes( + aggregateOperation as AGGREGATE_OPERATIONS, + ), ); return ( @@ -34,7 +39,9 @@ export const RecordTableColumnAggregateFooterDropdownContent = () => { case 'countAggregateOperationsOptions': { const aggregateOperations = availableAggregateOperations.filter( (aggregateOperation) => - COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), + COUNT_AGGREGATE_OPERATION_OPTIONS.includes( + aggregateOperation as AGGREGATE_OPERATIONS, + ), ); return ( { case 'percentAggregateOperationsOptions': { const aggregateOperations = availableAggregateOperations.filter( (aggregateOperation) => - PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), + PERCENT_AGGREGATE_OPERATION_OPTIONS.includes( + aggregateOperation as AGGREGATE_OPERATIONS, + ), ); return ( { /> ); } + case 'datesAggregateOperationsOptions': { + const aggregateOperations = availableAggregateOperations.filter( + (aggregateOperation) => + DATE_AGGREGATE_OPERATION_OPTIONS.includes( + aggregateOperation as DATE_AGGREGATE_OPERATIONS, + ), + ); + return ( + + ); + } default: return ; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent.tsx index a81e78288..e62862250 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent.tsx @@ -1,6 +1,8 @@ +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; 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 { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; 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 { useContext, useMemo } from 'react'; 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'; export const RecordTableColumnAggregateFooterMenuContent = () => { - const { fieldMetadataId, dropdownId, onContentChange } = useContext( - RecordTableColumnAggregateFooterDropdownContext, - ); + const { + fieldMetadataId, + dropdownId, + onContentChange, + fieldMetadataType, + resetContent, + } = useContext(RecordTableColumnAggregateFooterDropdownContext); const { closeDropdown } = useDropdown(dropdownId); const { objectMetadataItem } = useRecordTableContextOrThrow(); @@ -36,16 +43,24 @@ export const RecordTableColumnAggregateFooterMenuContent = () => { [fieldMetadataId, objectMetadataItem.fields], ); + const fieldIsDateKind = isFieldMetadataDateKind(fieldMetadataType); + const nonStandardAvailableAggregateOperation = - availableAggregateOperation.filter( - (aggregateOperation) => - !STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), + availableAggregateOperation.filter((aggregateOperation) => + NON_STANDARD_AGGREGATE_OPERATION_OPTIONS.includes( + aggregateOperation as AGGREGATE_OPERATIONS, + ), ); const fieldIsRelation = objectMetadataItem.fields.find((field) => field.id === fieldMetadataId) ?.type === FieldMetadataType.Relation; + const { + updateViewFieldAggregateOperation, + currentViewFieldAggregateOperation, + } = useViewFieldAggregateOperation(); + return ( <> @@ -65,6 +80,15 @@ export const RecordTableColumnAggregateFooterMenuContent = () => { hasSubMenu /> )} + {fieldIsDateKind && ( + { + onContentChange('datesAggregateOperationsOptions'); + }} + text={'Dates'} + hasSubMenu + /> + )} {nonStandardAvailableAggregateOperation.length > 0 ? ( { @@ -74,6 +98,21 @@ export const RecordTableColumnAggregateFooterMenuContent = () => { hasSubMenu /> ) : null} + { + updateViewFieldAggregateOperation(null); + resetContent(); + closeDropdown(); + }} + text={'None'} + RightIcon={ + !isDefined(currentViewFieldAggregateOperation) + ? IconCheck + : undefined + } + aria-selected={!isDefined(currentViewFieldAggregateOperation)} + /> ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx index e5f30bf30..3632c93c0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue.tsx @@ -36,8 +36,9 @@ const StyledValueContainer = styled(StyledScrollableContainer)` padding: 0 8px; `; -const StyledValue = styled(StyledScrollableContainer)` - color: ${({ theme }) => theme.color.gray60}; +const StyledValue = styled.div` + color: ${({ theme }) => theme.font.color.primary}; + max-width: 100%; `; export const RecordTableColumnAggregateFooterValue = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx index 421ee8245..3d8cf824a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx @@ -76,7 +76,9 @@ export const RecordTableColumnAggregateFooterValueCell = ({ fieldMetadataId={fieldMetadataId} dropdownId={dropdownId} /> - + {!hasAggregateOperationForViewField && ( + + )} ) : ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/dateAggregateOperationOptions.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/dateAggregateOperationOptions.tsx new file mode 100644 index 000000000..25de7a835 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/dateAggregateOperationOptions.tsx @@ -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, +]; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions.tsx similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions.tsx index 46b9bdbde..7f16154d1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/standardAggregateOperationOptions.tsx @@ -1,5 +1,5 @@ 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 = [ ...COUNT_AGGREGATE_OPERATION_OPTIONS, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation.tsx index ebfaa1f52..7cfa13862 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation.tsx @@ -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 { 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 { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useContext } from 'react'; @@ -18,13 +19,19 @@ export const useViewFieldAggregateOperation = () => { ); const { updateViewFieldRecords } = usePersistViewFieldRecords(); const updateViewFieldAggregateOperation = ( - aggregateOperation: AGGREGATE_OPERATIONS | null, + aggregateOperation: ExtendedAggregateOperations | null, ) => { if (!currentViewField) { throw new Error('ViewField not found'); } updateViewFieldRecords([ - { ...currentViewField, aggregateOperation: aggregateOperation }, + { + ...currentViewField, + aggregateOperation: + convertExtendedAggregateOperationToAggregateOperation( + aggregateOperation, + ), + }, ]); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState.ts index babe48606..8ab626e49 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState.ts @@ -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'; export const viewFieldAggregateOperationState = createFamilyState< - AGGREGATE_OPERATIONS | null | undefined, + ExtendedAggregateOperations | null | undefined, { viewFieldId: string } >({ key: 'viewFieldAggregateOperationState', diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId.tsx index 72e870622..c8884ddd1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId.tsx @@ -1,4 +1,5 @@ export type RecordTableFooterAggregateContentId = | 'moreAggregateOperationOptions' | 'countAggregateOperationsOptions' - | 'percentAggregateOperationsOptions'; + | 'percentAggregateOperationsOptions' + | 'datesAggregateOperationsOptions'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType.ts index 9b44db458..343bd3887 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType.ts @@ -1,5 +1,6 @@ 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 { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { AggregateOperationsOmittingStandardOperations } from '@/object-record/types/AggregateOperationsOmittingStandardOperations'; import { isFieldTypeValidForAggregateOperation } from '@/object-record/utils/isFieldTypeValidForAggregateOperation'; import { FieldMetadataType } from '~/generated/graphql'; @@ -14,7 +15,7 @@ export const getAvailableAggregateOperationsForFieldMetadataType = ({ return [AGGREGATE_OPERATIONS.count]; } - const availableAggregateOperations = new Set([ + const availableAggregateOperations = new Set([ AGGREGATE_OPERATIONS.count, AGGREGATE_OPERATIONS.countEmpty, AGGREGATE_OPERATIONS.countNotEmpty, @@ -35,7 +36,9 @@ export const getAvailableAggregateOperationsForFieldMetadataType = ({ ), ) .forEach((operation) => - availableAggregateOperations.add(operation as AGGREGATE_OPERATIONS), + availableAggregateOperations.add( + operation as ExtendedAggregateOperations, + ), ); return Array.from(availableAggregateOperations); diff --git a/packages/twenty-front/src/modules/object-record/record-table/types/ExtendedAggregateOperations.ts b/packages/twenty-front/src/modules/object-record/record-table/types/ExtendedAggregateOperations.ts index 18ba7e83f..6f0373182 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/types/ExtendedAggregateOperations.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/types/ExtendedAggregateOperations.ts @@ -1,6 +1,6 @@ import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { DATE_AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/DateAggregateOperations'; export type ExtendedAggregateOperations = | AGGREGATE_OPERATIONS - | 'EARLIEST' - | 'LATEST'; + | DATE_AGGREGATE_OPERATIONS; diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/getAvailableFieldsIdsForAggregationFromObjectFields.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/getAvailableFieldsIdsForAggregationFromObjectFields.test.ts index 01880c2a2..485cfc23c 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/getAvailableFieldsIdsForAggregationFromObjectFields.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/getAvailableFieldsIdsForAggregationFromObjectFields.test.ts @@ -2,7 +2,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; 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 { 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 { FieldMetadataType } from '~/generated/graphql'; diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/initializeAvailableFieldsForAggregateOperationMap.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/initializeAvailableFieldsForAggregateOperationMap.test.ts index 67e1e26ba..1f1bd9446 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/initializeAvailableFieldsForAggregateOperationMap.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/initializeAvailableFieldsForAggregateOperationMap.test.ts @@ -1,7 +1,7 @@ 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 { 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 { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap'; diff --git a/packages/twenty-front/src/modules/object-record/utils/convertAggregateOperationToExtendedAggregateOperation.ts b/packages/twenty-front/src/modules/object-record/utils/convertAggregateOperationToExtendedAggregateOperation.ts index e2d3ccd45..11b6e77ca 100644 --- a/packages/twenty-front/src/modules/object-record/utils/convertAggregateOperationToExtendedAggregateOperation.ts +++ b/packages/twenty-front/src/modules/object-record/utils/convertAggregateOperationToExtendedAggregateOperation.ts @@ -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'; import { isFieldMetadataDateKind } from 'twenty-shared'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -9,10 +10,10 @@ export const convertAggregateOperationToExtendedAggregateOperation = ( ): ExtendedAggregateOperations => { if (isFieldMetadataDateKind(fieldType) === true) { if (aggregateOperation === AGGREGATE_OPERATIONS.min) { - return 'EARLIEST'; + return DATE_AGGREGATE_OPERATIONS.earliest; } if (aggregateOperation === AGGREGATE_OPERATIONS.max) { - return 'LATEST'; + return DATE_AGGREGATE_OPERATIONS.latest; } } return aggregateOperation; diff --git a/packages/twenty-front/src/modules/object-record/utils/convertExtendedAggregateOperationToAggregateOperation.ts b/packages/twenty-front/src/modules/object-record/utils/convertExtendedAggregateOperationToAggregateOperation.ts index 2f1055f42..f492c5ebc 100644 --- a/packages/twenty-front/src/modules/object-record/utils/convertExtendedAggregateOperationToAggregateOperation.ts +++ b/packages/twenty-front/src/modules/object-record/utils/convertExtendedAggregateOperationToAggregateOperation.ts @@ -1,14 +1,15 @@ 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 convertExtendedAggregateOperationToAggregateOperation = ( - extendedAggregateOperation: ExtendedAggregateOperations, + extendedAggregateOperation: ExtendedAggregateOperations | null, ) => { - if (extendedAggregateOperation === 'EARLIEST') { + if (extendedAggregateOperation === DATE_AGGREGATE_OPERATIONS.earliest) { return AGGREGATE_OPERATIONS.min; } - if (extendedAggregateOperation === 'LATEST') { + if (extendedAggregateOperation === DATE_AGGREGATE_OPERATIONS.latest) { return AGGREGATE_OPERATIONS.max; } return extendedAggregateOperation; diff --git a/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts b/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts index 17d9bc503..520af3697 100644 --- a/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts +++ b/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts @@ -1,10 +1,12 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; 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 { FieldMetadataType } from '~/generated-metadata/graphql'; type NameForAggregation = { - [T in AGGREGATE_OPERATIONS]?: string; + [T in ExtendedAggregateOperations]?: string; }; type Aggregations = { @@ -58,8 +60,8 @@ export const getAvailableAggregationsFromObjectFields = ( if (isFieldMetadataDateKind(field.type) === true) { acc[field.name] = { ...acc[field.name], - [AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`, - [AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`, + [DATE_AGGREGATE_OPERATIONS.earliest]: `min${capitalize(field.name)}`, + [DATE_AGGREGATE_OPERATIONS.latest]: `max${capitalize(field.name)}`, }; } diff --git a/packages/twenty-front/src/modules/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields.ts b/packages/twenty-front/src/modules/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields.ts index f225986c5..91b689134 100644 --- a/packages/twenty-front/src/modules/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields.ts +++ b/packages/twenty-front/src/modules/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields.ts @@ -1,15 +1,13 @@ 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 { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation'; -import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields'; import { initializeAvailableFieldsForAggregateOperationMap } from '@/object-record/utils/initializeAvailableFieldsForAggregateOperationMap'; import { isDefined } from '~/utils/isDefined'; export const getAvailableFieldsIdsForAggregationFromObjectFields = ( fields: FieldMetadataItem[], - targetAggregateOperations: AGGREGATE_OPERATIONS[], + targetAggregateOperations: ExtendedAggregateOperations[], ): AvailableFieldsForAggregateOperation => { const aggregationMap = initializeAvailableFieldsForAggregateOperationMap( targetAggregateOperations, @@ -20,20 +18,12 @@ export const getAvailableFieldsIdsForAggregationFromObjectFields = ( return fields.reduce((acc, field) => { if (isDefined(allAggregations[field.name])) { Object.keys(allAggregations[field.name]).forEach((aggregation) => { - if ( - targetAggregateOperations.includes( - aggregation as AGGREGATE_OPERATIONS, - ) - ) { - const convertedAggregateOperation: ExtendedAggregateOperations = - convertAggregateOperationToExtendedAggregateOperation( - aggregation as AGGREGATE_OPERATIONS, - field.type, - ); - if (!isDefined(acc[convertedAggregateOperation])) { - acc[convertedAggregateOperation] = []; + const typedAggregation = aggregation as ExtendedAggregateOperations; + if (targetAggregateOperations.includes(typedAggregation)) { + if (!isDefined(acc[typedAggregation])) { + acc[typedAggregation] = []; } - (acc[convertedAggregateOperation] as string[]).push(field.id); + (acc[typedAggregation] as string[]).push(field.id); } }); } diff --git a/packages/twenty-front/src/modules/object-record/utils/initializeAvailableFieldsForAggregateOperationMap.ts b/packages/twenty-front/src/modules/object-record/utils/initializeAvailableFieldsForAggregateOperationMap.ts index 71f70a211..86b1cd56d 100644 --- a/packages/twenty-front/src/modules/object-record/utils/initializeAvailableFieldsForAggregateOperationMap.ts +++ b/packages/twenty-front/src/modules/object-record/utils/initializeAvailableFieldsForAggregateOperationMap.ts @@ -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 { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; export const initializeAvailableFieldsForAggregateOperationMap = ( - aggregateOperations: AGGREGATE_OPERATIONS[], + aggregateOperations: ExtendedAggregateOperations[], ): AvailableFieldsForAggregateOperation => { return aggregateOperations.reduce( (acc, operation) => ({ ...acc, - [convertAggregateOperationToExtendedAggregateOperation(operation)]: [], + [operation]: [], }), {}, ); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index ee55b31c1..6b266e49e 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -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 { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState'; +import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; 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 { + Placement, autoUpdate, flip, offset, - Placement, size, useFloating, } from '@floating-ui/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 { Keys } from 'react-hotkeys-hook'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-ui'; import { sleep } from '~/utils/sleep'; -import { DropdownOnToggleEffect } from './DropdownOnToggleEffect'; +import { useDropdown } from '../hooks/useDropdown'; const StyledDropdownFallbackAnchor = styled.div` left: 0; @@ -32,6 +30,26 @@ const StyledDropdownFallbackAnchor = styled.div` top: 0; `; +type StyledHeaderDivProps = { + isUnfolded?: boolean; + isActive?: boolean; +}; + +const StyledHeaderDiv = styled.div` + & 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 = { className?: string; clickableComponent?: ReactNode; @@ -133,16 +151,17 @@ export const Dropdown = ({ <> {isDefined(clickableComponent) ? ( -
{clickableComponent} -
+ ) : ( )} diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/StyledHeaderDropdownButton.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/StyledHeaderDropdownButton.tsx index ba6658ff5..bc0373c93 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/StyledHeaderDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/StyledHeaderDropdownButton.tsx @@ -8,8 +8,6 @@ type StyledDropdownButtonProps = { export const StyledHeaderDropdownButton = styled.button` font-family: inherit; align-items: center; - background: ${({ theme, isUnfolded }) => - isUnfolded ? theme.background.transparent.light : theme.background.primary}; border: none; border-radius: ${({ theme }) => theme.border.radius.sm}; color: ${({ isActive, theme }) => @@ -22,11 +20,4 @@ export const StyledHeaderDropdownButton = styled.button theme.spacing(2)}; user-select: none; - - &:hover { - background: ${({ theme, isUnfolded }) => - isUnfolded - ? theme.background.transparent.medium - : theme.background.transparent.light}; - } `; diff --git a/packages/twenty-front/src/modules/views/hooks/useUpdateViewAggregate.ts b/packages/twenty-front/src/modules/views/hooks/useUpdateViewAggregate.ts index 0dface682..c9924120a 100644 --- a/packages/twenty-front/src/modules/views/hooks/useUpdateViewAggregate.ts +++ b/packages/twenty-front/src/modules/views/hooks/useUpdateViewAggregate.ts @@ -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 { useUpdateView } from '@/views/hooks/useUpdateView'; import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; @@ -13,13 +14,18 @@ export const useUpdateViewAggregate = () => { kanbanAggregateOperation, }: { kanbanAggregateOperationFieldMetadataId: string | null; - kanbanAggregateOperation: AGGREGATE_OPERATIONS | null; - }) => + kanbanAggregateOperation: ExtendedAggregateOperations | null; + }) => { + const convertedKanbanAggregateOperation = + convertExtendedAggregateOperationToAggregateOperation( + kanbanAggregateOperation, + ); updateView({ id: currentViewId, kanbanAggregateOperationFieldMetadataId, - kanbanAggregateOperation, - }), + kanbanAggregateOperation: convertedKanbanAggregateOperation, + }); + }, [currentViewId, updateView], ); diff --git a/packages/twenty-shared/src/utils/fieldMetadata/isFieldMetadataDateKind.ts b/packages/twenty-shared/src/utils/fieldMetadata/isFieldMetadataDateKind.ts index b81943a06..f28882422 100644 --- a/packages/twenty-shared/src/utils/fieldMetadata/isFieldMetadataDateKind.ts +++ b/packages/twenty-shared/src/utils/fieldMetadata/isFieldMetadataDateKind.ts @@ -1,7 +1,7 @@ import { FieldMetadataType } from 'src/types/FieldMetadataType'; export const isFieldMetadataDateKind = ( - fieldMetadataType: FieldMetadataType, + fieldMetadataType?: FieldMetadataType, ): fieldMetadataType is | FieldMetadataType.DATE | FieldMetadataType.DATE_TIME => {