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 855116d1a..def721618 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,6 +4,7 @@ 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,6 +48,8 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { if (!isDefined(aggregateOperation)) return <>; + const convertedAggregateOperation = + convertExtendedAggregateOperationToAggregateOperation(aggregateOperation); return ( <> { onClick={() => { updateViewAggregate({ kanbanAggregateOperationFieldMetadataId: fieldId, - kanbanAggregateOperation: aggregateOperation, + kanbanAggregateOperation: convertedAggregateOperation, }); closeDropdown(); }} @@ -82,7 +85,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { recordIndexKanbanAggregateOperation?.fieldMetadataId === fieldId && recordIndexKanbanAggregateOperation?.operation === - aggregateOperation + convertedAggregateOperation ? IconCheck : undefined } diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx index 2145c4efa..1affb3a38 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx @@ -8,6 +8,7 @@ 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 { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; @@ -69,7 +70,7 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({ availableAggregationOperation !== AGGREGATE_OPERATIONS.count ) { setAggregateOperation( - availableAggregationOperation as AGGREGATE_OPERATIONS, + availableAggregationOperation as ExtendedAggregateOperations, ); setAvailableFieldsForAggregateOperation( @@ -87,7 +88,7 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({ } }} text={getAggregateOperationLabel( - availableAggregationOperation as AGGREGATE_OPERATIONS, + availableAggregationOperation as ExtendedAggregateOperations, )} hasSubMenu={ availableAggregationOperation === AGGREGATE_OPERATIONS.count diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts index 8810807af..cdc7248a8 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts @@ -9,6 +9,7 @@ import { recordIndexFiltersState } from '@/object-record/record-index/states/rec import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; +import { UserContext } from '@/users/contexts/UserContext'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; @@ -80,12 +81,17 @@ export const useAggregateRecordsForRecordBoardColumn = () => { skip: !isAggregateQueryEnabled, }); + const { dateFormat, timeFormat, timeZone } = useContext(UserContext); + const { value, labelWithFieldName } = computeAggregateValueAndLabel({ data, objectMetadataItem, fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId, aggregateOperation: recordIndexKanbanAggregateOperation?.operation, fallbackFieldName: kanbanFieldName, + dateFormat, + timeFormat, + timeZone, }); return { diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/aggregateOperationComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/aggregateOperationComponentState.ts index 9b10c7caf..01dcbd5f4 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/aggregateOperationComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/aggregateOperationComponentState.ts @@ -1,9 +1,9 @@ import { RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext } from '@/object-record/record-board/contexts/RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext'; -import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const aggregateOperationComponentState = - createComponentStateV2({ + createComponentStateV2({ key: 'aggregateOperationComponentFamilyState', defaultValue: null, componentInstanceContext: 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 0af203810..1b588ab70 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 @@ -1,5 +1,8 @@ +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; 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 { FieldMetadataType } from '~/generated/graphql'; @@ -20,13 +23,20 @@ describe('computeAggregateValueAndLabel', () => { ], } as ObjectMetadataItem; + const defaultParams = { + dateFormat: DateFormat.DAY_FIRST, + timeFormat: TimeFormat.HOUR_24, + timeZone: 'UTC', + }; + it('should return empty object for empty data', () => { const result = computeAggregateValueAndLabel({ - data: {}, + data: {} as AggregateRecordsData, objectMetadataItem: mockObjectMetadata, fieldMetadataId: MOCK_FIELD_ID, aggregateOperation: AGGREGATE_OPERATIONS.sum, fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + ...defaultParams, }); expect(result).toEqual({}); @@ -37,7 +47,7 @@ describe('computeAggregateValueAndLabel', () => { amount: { [AGGREGATE_OPERATIONS.sum]: 2000000, }, - }; + } as AggregateRecordsData; const result = computeAggregateValueAndLabel({ data: mockData, @@ -45,6 +55,7 @@ describe('computeAggregateValueAndLabel', () => { fieldMetadataId: MOCK_FIELD_ID, aggregateOperation: AGGREGATE_OPERATIONS.sum, fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + ...defaultParams, }); expect(result).toEqual({ @@ -74,7 +85,7 @@ describe('computeAggregateValueAndLabel', () => { percentage: { [AGGREGATE_OPERATIONS.avg]: 0.3, }, - }; + } as AggregateRecordsData; const result = computeAggregateValueAndLabel({ data: mockData, @@ -82,6 +93,7 @@ describe('computeAggregateValueAndLabel', () => { fieldMetadataId: MOCK_FIELD_ID, aggregateOperation: AGGREGATE_OPERATIONS.avg, fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + ...defaultParams, }); expect(result).toEqual({ @@ -111,7 +123,7 @@ describe('computeAggregateValueAndLabel', () => { decimals: { [AGGREGATE_OPERATIONS.sum]: 0.009, }, - }; + } as AggregateRecordsData; const result = computeAggregateValueAndLabel({ data: mockData, @@ -119,6 +131,7 @@ describe('computeAggregateValueAndLabel', () => { fieldMetadataId: MOCK_FIELD_ID, aggregateOperation: AGGREGATE_OPERATIONS.sum, fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + ...defaultParams, }); expect(result).toEqual({ @@ -128,17 +141,88 @@ describe('computeAggregateValueAndLabel', () => { }); }); + it('should handle datetime field with min operation', () => { + const mockObjectMetadataWithDatetimeField: ObjectMetadataItem = { + id: '123', + fields: [ + { + id: MOCK_FIELD_ID, + name: 'createdAt', + label: 'Created At', + type: FieldMetadataType.DateTime, + } as FieldMetadataItem, + ], + } as ObjectMetadataItem; + + const mockData = { + createdAt: { + [AGGREGATE_OPERATIONS.min]: '2023-01-01T12:00:00Z', + }, + } as AggregateRecordsData; + + const result = computeAggregateValueAndLabel({ + data: mockData, + objectMetadataItem: mockObjectMetadataWithDatetimeField, + fieldMetadataId: MOCK_FIELD_ID, + aggregateOperation: AGGREGATE_OPERATIONS.min, + fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + ...defaultParams, + }); + + expect(result).toEqual({ + label: 'Earliest date', + labelWithFieldName: 'Earliest date of Created At', + value: '1 Jan, 2023 12:00', + }); + }); + + it('should handle datetime field with max operation', () => { + const mockObjectMetadataWithDatetimeField: ObjectMetadataItem = { + id: '123', + fields: [ + { + id: MOCK_FIELD_ID, + name: 'updatedAt', + label: 'Updated At', + type: FieldMetadataType.DateTime, + } as FieldMetadataItem, + ], + } as ObjectMetadataItem; + + const mockData = { + updatedAt: { + [AGGREGATE_OPERATIONS.max]: '2023-12-31T23:59:59Z', + }, + } as AggregateRecordsData; + + const result = computeAggregateValueAndLabel({ + data: mockData, + objectMetadataItem: mockObjectMetadataWithDatetimeField, + fieldMetadataId: MOCK_FIELD_ID, + aggregateOperation: AGGREGATE_OPERATIONS.max, + fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + ...defaultParams, + }); + + expect(result).toEqual({ + value: '31 Dec, 2023 23:59', + label: 'Latest date', + labelWithFieldName: 'Latest date of Updated At', + }); + }); + it('should default to count when field not found', () => { const mockData = { [MOCK_KANBAN_FIELD_NAME]: { [AGGREGATE_OPERATIONS.count]: 42, }, - }; + } as AggregateRecordsData; const result = computeAggregateValueAndLabel({ data: mockData, objectMetadataItem: mockObjectMetadata, fallbackFieldName: MOCK_KANBAN_FIELD_NAME, + ...defaultParams, }); expect(result).toEqual({ @@ -153,13 +237,14 @@ describe('computeAggregateValueAndLabel', () => { amount: { [AGGREGATE_OPERATIONS.sum]: undefined, }, - }; + } as AggregateRecordsData; const result = computeAggregateValueAndLabel({ data: mockData, objectMetadataItem: mockObjectMetadata, fieldMetadataId: MOCK_FIELD_ID, aggregateOperation: AGGREGATE_OPERATIONS.sum, + ...defaultParams, }); expect(result).toEqual({ 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 de7afc3cc..0893212f6 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 { expect } from '@storybook/test'; describe('getAggregateOperationLabel', () => { it('should return correct labels for each operation', () => { @@ -8,6 +9,8 @@ describe('getAggregateOperationLabel', () => { expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.avg)).toBe( 'Average', ); + expect(getAggregateOperationLabel('EARLIEST')).toBe('Earliest date'); + expect(getAggregateOperationLabel('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 f78945028..d50e74f33 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 @@ -1,14 +1,18 @@ +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions'; import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption'; +import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; import isEmpty from 'lodash.isempty'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { formatAmount } from '~/utils/format/formatAmount'; import { formatNumber } from '~/utils/format/number'; import { isDefined } from '~/utils/isDefined'; +import { formatDateString } from '~/utils/string/formatDateString'; export const computeAggregateValueAndLabel = ({ data, @@ -16,12 +20,18 @@ export const computeAggregateValueAndLabel = ({ fieldMetadataId, aggregateOperation, fallbackFieldName, + dateFormat, + timeFormat, + timeZone, }: { data: AggregateRecordsData; objectMetadataItem: ObjectMetadataItem; fieldMetadataId?: string | null; aggregateOperation?: AGGREGATE_OPERATIONS | null; fallbackFieldName?: string; + dateFormat: DateFormat; + timeFormat: TimeFormat; + timeZone: string; }) => { if (isEmpty(data)) { return {}; @@ -49,6 +59,8 @@ export const computeAggregateValueAndLabel = ({ let value; + const displayAsRelativeDate = field?.settings?.displayAsRelativeDate; + if (COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) { value = aggregateValue; } else if (!isDefined(aggregateValue)) { @@ -56,15 +68,15 @@ export const computeAggregateValueAndLabel = ({ } else if (PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) { value = `${formatNumber(Number(aggregateValue) * 100)}%`; } else { - value = Number(aggregateValue); - switch (field.type) { case FieldMetadataType.Currency: { + value = Number(aggregateValue); value = formatAmount(value / 1_000_000); break; } case FieldMetadataType.Number: { + value = Number(aggregateValue); const { decimals, type } = field.settings ?? {}; value = type === 'percentage' @@ -72,13 +84,30 @@ export const computeAggregateValueAndLabel = ({ : formatNumber(value, decimals); break; } + + case FieldMetadataType.DateTime: { + value = aggregateValue as string; + value = formatDateString({ + value, + displayAsRelativeDate, + timeZone, + dateFormat, + timeFormat, + }); + break; + } } } - const label = getAggregateOperationLabel(aggregateOperation); + const convertedAggregateOperation = + convertAggregateOperationToExtendedAggregateOperation( + aggregateOperation, + field.type, + ); + const label = getAggregateOperationLabel(convertedAggregateOperation); const labelWithFieldName = aggregateOperation === AGGREGATE_OPERATIONS.count ? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}` - : `${getAggregateOperationLabel(aggregateOperation)} of ${field.label}`; + : `${getAggregateOperationLabel(convertedAggregateOperation)} 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 d2b969e31..74f492e86 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,6 +1,9 @@ import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; -export const getAggregateOperationLabel = (operation: AGGREGATE_OPERATIONS) => { +export const getAggregateOperationLabel = ( + operation: ExtendedAggregateOperations, +) => { switch (operation) { case AGGREGATE_OPERATIONS.min: return 'Min'; @@ -22,6 +25,10 @@ export const getAggregateOperationLabel = (operation: AGGREGATE_OPERATIONS) => { return 'Percent empty'; case AGGREGATE_OPERATIONS.percentageNotEmpty: return 'Percent not empty'; + case 'EARLIEST': + return 'Earliest date'; + case 'LATEST': + return 'Latest date'; default: throw new Error(`Unknown aggregate operation: ${operation}`); } 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 3b6f62738..c6c413cfc 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 @@ -5,10 +5,12 @@ export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = { [AGGREGATE_OPERATIONS.min]: [ FieldMetadataType.Number, FieldMetadataType.Currency, + FieldMetadataType.DateTime, ], [AGGREGATE_OPERATIONS.max]: [ FieldMetadataType.Number, FieldMetadataType.Currency, + FieldMetadataType.DateTime, ], [AGGREGATE_OPERATIONS.avg]: [ FieldMetadataType.Number, 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 e32103872..75f51312d 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 @@ -2,6 +2,7 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record- import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; import { useViewFieldAggregateOperation } from '@/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation'; +import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { ReactNode, useContext } from 'react'; import { IconCheck, isDefined, MenuItem } from 'twenty-ui'; @@ -18,7 +19,7 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({ currentViewFieldAggregateOperation, } = useViewFieldAggregateOperation(); - const { dropdownId, resetContent } = useContext( + const { dropdownId, resetContent, fieldMetadataType } = useContext( RecordTableColumnAggregateFooterDropdownContext, ); const { closeDropdown } = useDropdown(dropdownId); @@ -31,7 +32,12 @@ export const RecordTableColumnAggregateFooterAggregateOperationMenuItems = ({ updateViewFieldAggregateOperation(operation); closeDropdown(); }} - text={getAggregateOperationLabel(operation)} + text={getAggregateOperationLabel( + convertAggregateOperationToExtendedAggregateOperation( + operation, + fieldMetadataType, + ), + )} 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 8fccf398a..c362d11d5 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,5 +1,4 @@ import { useDropdown } from '@/dropdown/hooks/useDropdown'; -import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableColumnAggregateFooterDropdownSubmenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateDropdownSubmenuContent'; import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; import { RecordTableColumnAggregateFooterMenuContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterMenuContent'; @@ -9,19 +8,13 @@ import { STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-tab import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType'; export const RecordTableColumnAggregateFooterDropdownContent = () => { - const { currentContentId, fieldMetadataId } = useDropdown({ + const { currentContentId, fieldMetadataType } = useDropdown({ context: RecordTableColumnAggregateFooterDropdownContext, }); - const { objectMetadataItem } = useRecordTableContextOrThrow(); - - const fieldMetadata = objectMetadataItem.fields.find( - (field) => field.id === fieldMetadataId, - ); - const availableAggregateOperations = getAvailableAggregateOperationsForFieldMetadataType({ - fieldMetadataType: fieldMetadata?.type, + fieldMetadataType: fieldMetadataType, }); switch (currentContentId) { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext.tsx index e8d108995..0be83af05 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext.tsx @@ -1,5 +1,6 @@ import { RecordTableFooterAggregateContentId } from '@/object-record/record-table/record-table-footer/types/RecordTableFooterAggregateContentId'; import { createContext } from 'react'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; export type RecordTableColumnAggregateFooterDropdownContextValue = { currentContentId: RecordTableFooterAggregateContentId | null; @@ -7,6 +8,7 @@ export type RecordTableColumnAggregateFooterDropdownContextValue = { resetContent: () => void; dropdownId: string; fieldMetadataId: string; + fieldMetadataType?: FieldMetadataType; }; export const RecordTableColumnAggregateFooterDropdownContext = 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 6ddaece2a..a81e78288 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 @@ -36,10 +36,11 @@ export const RecordTableColumnAggregateFooterMenuContent = () => { [fieldMetadataId, objectMetadataItem.fields], ); - const otherAvailableAggregateOperation = availableAggregateOperation.filter( - (aggregateOperation) => - !STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), - ); + const nonStandardAvailableAggregateOperation = + availableAggregateOperation.filter( + (aggregateOperation) => + !STANDARD_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation), + ); const fieldIsRelation = objectMetadataItem.fields.find((field) => field.id === fieldMetadataId) @@ -64,7 +65,7 @@ export const RecordTableColumnAggregateFooterMenuContent = () => { hasSubMenu /> )} - {otherAvailableAggregateOperation.length > 0 ? ( + {nonStandardAvailableAggregateOperation.length > 0 ? ( { onContentChange('moreAggregateOperationOptions'); 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 0294770f1..e5f30bf30 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 @@ -16,19 +16,28 @@ const StyledText = styled.span` z-index: 1; `; -const StyledValueContainer = styled.div` +const StyledScrollableContainer = styled.div` + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } +`; + +const StyledValueContainer = styled(StyledScrollableContainer)` align-items: center; display: flex; - flex: 1 0 0; gap: 4px; height: 32px; justify-content: flex-end; padding: 0 8px; `; -const StyledValue = styled.div` +const StyledValue = styled(StyledScrollableContainer)` color: ${({ theme }) => theme.color.gray60}; - flex: 1 0 0; `; 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 151da5908..b6f91db7c 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 @@ -29,6 +29,7 @@ const StyledIcon = styled(IconChevronDown)` height: 20px; justify-content: center; flex-grow: 0; + flex-shrink: 0; padding-right: ${({ theme }) => theme.spacing(2)}; `; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx index a0062384c..341394998 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown.tsx @@ -1,4 +1,5 @@ import { useCurrentContentId } from '@/dropdown/hooks/useCurrentContentId'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { RecordTableColumnAggregateFooterDropdownContent } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContent'; import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext'; @@ -24,6 +25,12 @@ export const RecordTableColumnFooterWithDropdown = ({ RecordTableColumnAggregateFooterCellContext, ); + const { objectMetadataItem } = useRecordTableContextOrThrow(); + + const fieldMetadata = objectMetadataItem.fields.find( + (field) => field.id === fieldMetadataId, + ); + const { toggleScrollXWrapper, toggleScrollYWrapper } = useToggleScrollWrapper(); @@ -61,6 +68,7 @@ export const RecordTableColumnFooterWithDropdown = ({ resetContent: handleResetContent, dropdownId: dropdownId, fieldMetadataId: fieldMetadataId, + fieldMetadataType: fieldMetadata?.type, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx index c5b63261d..dc8d97e4d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx @@ -8,6 +8,7 @@ import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/s import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; +import { UserContext } from '@/users/contexts/UserContext'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; @@ -64,11 +65,16 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( !isAggregateQueryEnabled || !isDefined(aggregateOperationForViewField), }); + const { dateFormat, timeFormat, timeZone } = useContext(UserContext); + const { value, label } = computeAggregateValueAndLabel({ data, objectMetadataItem, fieldMetadataId: fieldMetadataId, aggregateOperation: aggregateOperationForViewField, + dateFormat, + timeFormat, + timeZone, }); return { 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 new file mode 100644 index 000000000..18ba7e83f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/types/ExtendedAggregateOperations.ts @@ -0,0 +1,6 @@ +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; + +export type ExtendedAggregateOperations = + | AGGREGATE_OPERATIONS + | 'EARLIEST' + | 'LATEST'; diff --git a/packages/twenty-front/src/modules/object-record/types/AvailableFieldsForAggregateOperation.ts b/packages/twenty-front/src/modules/object-record/types/AvailableFieldsForAggregateOperation.ts index 8b3983a26..88247b119 100644 --- a/packages/twenty-front/src/modules/object-record/types/AvailableFieldsForAggregateOperation.ts +++ b/packages/twenty-front/src/modules/object-record/types/AvailableFieldsForAggregateOperation.ts @@ -1,5 +1,5 @@ -import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; export type AvailableFieldsForAggregateOperation = { - [T in AGGREGATE_OPERATIONS]?: string[]; + [T in ExtendedAggregateOperations]?: string[]; }; diff --git a/packages/twenty-front/src/modules/object-record/utils/convertAggregateOperationToExtendedAggregateOperation.ts b/packages/twenty-front/src/modules/object-record/utils/convertAggregateOperationToExtendedAggregateOperation.ts new file mode 100644 index 000000000..5df5d2caf --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/convertAggregateOperationToExtendedAggregateOperation.ts @@ -0,0 +1,18 @@ +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const convertAggregateOperationToExtendedAggregateOperation = ( + aggregateOperation: AGGREGATE_OPERATIONS, + fieldType?: FieldMetadataType, +): ExtendedAggregateOperations => { + if (fieldType === FieldMetadataType.DateTime) { + if (aggregateOperation === AGGREGATE_OPERATIONS.min) { + return 'EARLIEST'; + } + if (aggregateOperation === AGGREGATE_OPERATIONS.max) { + return '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 new file mode 100644 index 000000000..2f1055f42 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/convertExtendedAggregateOperationToAggregateOperation.ts @@ -0,0 +1,15 @@ +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; + +import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; + +export const convertExtendedAggregateOperationToAggregateOperation = ( + extendedAggregateOperation: ExtendedAggregateOperations, +) => { + if (extendedAggregateOperation === 'EARLIEST') { + return AGGREGATE_OPERATIONS.min; + } + if (extendedAggregateOperation === '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 43b26fc8e..541b63343 100644 --- a/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts +++ b/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts @@ -55,6 +55,14 @@ export const getAvailableAggregationsFromObjectFields = ( }; } + if (field.type === FieldMetadataType.DateTime) { + acc[field.name] = { + ...acc[field.name], + [AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`, + [AGGREGATE_OPERATIONS.max]: `max${capitalize(field.name)}`, + }; + } + if (acc[field.name] === undefined) { acc[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 88bbc0698..f225986c5 100644 --- a/packages/twenty-front/src/modules/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields.ts +++ b/packages/twenty-front/src/modules/object-record/utils/getAvailableFieldsIdsForAggregationFromObjectFields.ts @@ -1,6 +1,8 @@ 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'; @@ -18,10 +20,20 @@ export const getAvailableFieldsIdsForAggregationFromObjectFields = ( return fields.reduce((acc, field) => { if (isDefined(allAggregations[field.name])) { Object.keys(allAggregations[field.name]).forEach((aggregation) => { - const typedAggregateOperation = aggregation as AGGREGATE_OPERATIONS; - - if (targetAggregateOperations.includes(typedAggregateOperation)) { - acc[typedAggregateOperation]?.push(field.id); + if ( + targetAggregateOperations.includes( + aggregation as AGGREGATE_OPERATIONS, + ) + ) { + const convertedAggregateOperation: ExtendedAggregateOperations = + convertAggregateOperationToExtendedAggregateOperation( + aggregation as AGGREGATE_OPERATIONS, + field.type, + ); + if (!isDefined(acc[convertedAggregateOperation])) { + acc[convertedAggregateOperation] = []; + } + (acc[convertedAggregateOperation] 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 d1e65967e..71f70a211 100644 --- a/packages/twenty-front/src/modules/object-record/utils/initializeAvailableFieldsForAggregateOperationMap.ts +++ b/packages/twenty-front/src/modules/object-record/utils/initializeAvailableFieldsForAggregateOperationMap.ts @@ -1,5 +1,6 @@ import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { AvailableFieldsForAggregateOperation } from '@/object-record/types/AvailableFieldsForAggregateOperation'; +import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; export const initializeAvailableFieldsForAggregateOperationMap = ( aggregateOperations: AGGREGATE_OPERATIONS[], @@ -7,7 +8,7 @@ export const initializeAvailableFieldsForAggregateOperationMap = ( return aggregateOperations.reduce( (acc, operation) => ({ ...acc, - [operation]: [], + [convertAggregateOperationToExtendedAggregateOperation(operation)]: [], }), {}, ); diff --git a/packages/twenty-front/src/modules/ui/field/display/components/DateTimeDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/DateTimeDisplay.tsx index 7f2432639..fc0139e76 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/DateTimeDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/DateTimeDisplay.tsx @@ -1,7 +1,6 @@ -import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime'; -import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate'; import { UserContext } from '@/users/contexts/UserContext'; import { useContext } from 'react'; +import { formatDateString } from '~/utils/string/formatDateString'; import { EllipsisDisplay } from './EllipsisDisplay'; type DateTimeDisplayProps = { @@ -15,11 +14,13 @@ export const DateTimeDisplay = ({ }: DateTimeDisplayProps) => { const { dateFormat, timeFormat, timeZone } = useContext(UserContext); - const formattedDate = value - ? displayAsRelativeDate - ? formatDateISOStringToRelativeDate(value) - : formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat) - : ''; + const formattedDate = formatDateString({ + value, + displayAsRelativeDate, + timeZone, + dateFormat, + timeFormat, + }); return {formattedDate}; }; diff --git a/packages/twenty-front/src/utils/string/__tests__/formatDateString.test.ts b/packages/twenty-front/src/utils/string/__tests__/formatDateString.test.ts new file mode 100644 index 000000000..f4088b6e6 --- /dev/null +++ b/packages/twenty-front/src/utils/string/__tests__/formatDateString.test.ts @@ -0,0 +1,86 @@ +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { DateTime } from 'luxon'; +import { formatDateString } from '~/utils/string/formatDateString'; + +describe('formatDateString', () => { + const defaultParams = { + timeZone: 'UTC', + dateFormat: DateFormat.DAY_FIRST, + timeFormat: TimeFormat.HOUR_24, + }; + + it('should return empty string for null value', () => { + const result = formatDateString({ + ...defaultParams, + value: null, + }); + + expect(result).toBe(''); + }); + + it('should return empty string for undefined value', () => { + const result = formatDateString({ + ...defaultParams, + value: undefined, + }); + + expect(result).toBe(''); + }); + + it('should format date as relative when displayAsRelativeDate is true', () => { + const mockDate = DateTime.now().minus({ months: 2 }).toISO(); + const mockRelativeDate = '2 months ago'; + + jest.mock('@/localization/utils/formatDateISOStringToRelativeDate', () => ({ + formatDateISOStringToRelativeDate: jest + .fn() + .mockReturnValue(mockRelativeDate), + })); + + const result = formatDateString({ + ...defaultParams, + value: mockDate, + displayAsRelativeDate: true, + }); + + expect(result).toBe(mockRelativeDate); + }); + + it('should format date as datetime when displayAsRelativeDate is false', () => { + const mockDate = '2023-01-01T12:00:00Z'; + const mockFormattedDate = '1 Jan, 2023 12:00'; + + jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({ + formatDateISOStringToDateTime: jest + .fn() + .mockReturnValue(mockFormattedDate), + })); + + const result = formatDateString({ + ...defaultParams, + value: mockDate, + displayAsRelativeDate: false, + }); + + expect(result).toBe(mockFormattedDate); + }); + + it('should format date as datetime by default when displayAsRelativeDate is not provided', () => { + const mockDate = '2023-01-01T12:00:00Z'; + const mockFormattedDate = '1 Jan, 2023 12:00'; + + jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({ + formatDateISOStringToDateTime: jest + .fn() + .mockReturnValue(mockFormattedDate), + })); + + const result = formatDateString({ + ...defaultParams, + value: mockDate, + }); + + expect(result).toBe(mockFormattedDate); + }); +}); diff --git a/packages/twenty-front/src/utils/string/formatDateString.ts b/packages/twenty-front/src/utils/string/formatDateString.ts new file mode 100644 index 000000000..6aaec870a --- /dev/null +++ b/packages/twenty-front/src/utils/string/formatDateString.ts @@ -0,0 +1,26 @@ +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime'; +import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate'; + +export const formatDateString = ({ + value, + timeZone, + dateFormat, + timeFormat, + displayAsRelativeDate, +}: { + timeZone: string; + dateFormat: DateFormat; + timeFormat: TimeFormat; + value?: string | null; + displayAsRelativeDate?: boolean; +}) => { + const formattedDate = value + ? displayAsRelativeDate + ? formatDateISOStringToRelativeDate(value) + : formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat) + : ''; + + return formattedDate; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts index 41c6f2401..52b75d50d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts @@ -79,7 +79,7 @@ export const getAvailableAggregationsFromObjectFields = ( case FieldMetadataType.DATE_TIME: acc[`min${capitalize(field.name)}`] = { type: GraphQLISODateTime, - description: `Oldest date contained in the field ${field.name}`, + description: `Earliest date contained in the field ${field.name}`, fromField: field.name, fromFieldType: field.type, aggregateOperation: AGGREGATE_OPERATIONS.min, @@ -87,7 +87,7 @@ export const getAvailableAggregationsFromObjectFields = ( acc[`max${capitalize(field.name)}`] = { type: GraphQLISODateTime, - description: `Most recent date contained in the field ${field.name}`, + description: `Latest date contained in the field ${field.name}`, fromField: field.name, fromFieldType: field.type, aggregateOperation: AGGREGATE_OPERATIONS.max,