Include Date fields in aggregate operations on dates (#9479)

Follow-up on https://github.com/twentyhq/twenty/pull/9444/files - I had
forgotten to include Date field types (in addition to DateTime)
This commit is contained in:
Marie
2025-01-09 13:13:21 +01:00
committed by GitHub
parent efb2a59e06
commit c535d21587
13 changed files with 176 additions and 40 deletions

View File

@ -13,6 +13,7 @@ import { formatAmount } from '~/utils/format/formatAmount';
import { formatNumber } from '~/utils/format/number';
import { isDefined } from '~/utils/isDefined';
import { formatDateString } from '~/utils/string/formatDateString';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
export const computeAggregateValueAndLabel = ({
data,
@ -87,7 +88,7 @@ export const computeAggregateValueAndLabel = ({
case FieldMetadataType.DateTime: {
value = aggregateValue as string;
value = formatDateString({
value = formatDateTimeString({
value,
displayAsRelativeDate,
timeZone,
@ -96,6 +97,17 @@ export const computeAggregateValueAndLabel = ({
});
break;
}
case FieldMetadataType.Date: {
value = aggregateValue as string;
value = formatDateString({
value,
displayAsRelativeDate,
timeZone,
dateFormat,
});
break;
}
}
}
const convertedAggregateOperation =

View File

@ -6,11 +6,13 @@ export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = {
FieldMetadataType.Number,
FieldMetadataType.Currency,
FieldMetadataType.DateTime,
FieldMetadataType.Date,
],
[AGGREGATE_OPERATIONS.max]: [
FieldMetadataType.Number,
FieldMetadataType.Currency,
FieldMetadataType.DateTime,
FieldMetadataType.Date,
],
[AGGREGATE_OPERATIONS.avg]: [
FieldMetadataType.Number,

View File

@ -1,12 +1,13 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { isFieldMetadataDateKind } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const convertAggregateOperationToExtendedAggregateOperation = (
aggregateOperation: AGGREGATE_OPERATIONS,
fieldType?: FieldMetadataType,
): ExtendedAggregateOperations => {
if (fieldType === FieldMetadataType.DateTime) {
if (isFieldMetadataDateKind(fieldType) === true) {
if (aggregateOperation === AGGREGATE_OPERATIONS.min) {
return 'EARLIEST';
}

View File

@ -1,6 +1,6 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { capitalize } from 'twenty-shared';
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type NameForAggregation = {
@ -55,7 +55,7 @@ export const getAvailableAggregationsFromObjectFields = (
};
}
if (field.type === FieldMetadataType.DateTime) {
if (isFieldMetadataDateKind(field.type) === true) {
acc[field.name] = {
...acc[field.name],
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,

View File

@ -1,7 +1,6 @@
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
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 DateDisplayProps = {
@ -15,11 +14,12 @@ export const DateDisplay = ({
}: DateDisplayProps) => {
const { dateFormat, timeZone } = useContext(UserContext);
const formattedDate = value
? displayAsRelativeDate
? formatDateISOStringToRelativeDate(value, true)
: formatDateISOStringToDate(value, timeZone, dateFormat)
: '';
const formattedDate = formatDateString({
value,
timeZone,
dateFormat,
displayAsRelativeDate,
});
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
};

View File

@ -1,6 +1,6 @@
import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import { formatDateString } from '~/utils/string/formatDateString';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
import { EllipsisDisplay } from './EllipsisDisplay';
type DateTimeDisplayProps = {
@ -14,7 +14,7 @@ export const DateTimeDisplay = ({
}: DateTimeDisplayProps) => {
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
const formattedDate = formatDateString({
const formattedDate = formatDateTimeString({
value,
displayAsRelativeDate,
timeZone,

View File

@ -1,5 +1,4 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { DateTime } from 'luxon';
import { formatDateString } from '~/utils/string/formatDateString';
@ -7,7 +6,6 @@ describe('formatDateString', () => {
const defaultParams = {
timeZone: 'UTC',
dateFormat: DateFormat.DAY_FIRST,
timeFormat: TimeFormat.HOUR_24,
};
it('should return empty string for null value', () => {
@ -49,7 +47,7 @@ describe('formatDateString', () => {
it('should format date as datetime when displayAsRelativeDate is false', () => {
const mockDate = '2023-01-01T12:00:00Z';
const mockFormattedDate = '1 Jan, 2023 12:00';
const mockFormattedDate = '1 Jan, 2023';
jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({
formatDateISOStringToDateTime: jest
@ -68,7 +66,7 @@ describe('formatDateString', () => {
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';
const mockFormattedDate = '1 Jan, 2023';
jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({
formatDateISOStringToDateTime: jest

View File

@ -0,0 +1,86 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { DateTime } from 'luxon';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
describe('formatDateTimeString', () => {
const defaultParams = {
timeZone: 'UTC',
dateFormat: DateFormat.DAY_FIRST,
timeFormat: TimeFormat.HOUR_24,
};
it('should return empty string for null value', () => {
const result = formatDateTimeString({
...defaultParams,
value: null,
});
expect(result).toBe('');
});
it('should return empty string for undefined value', () => {
const result = formatDateTimeString({
...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 = formatDateTimeString({
...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 = formatDateTimeString({
...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 = formatDateTimeString({
...defaultParams,
value: mockDate,
});
expect(result).toBe(mockFormattedDate);
});
});

View File

@ -1,25 +1,22 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime';
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
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)
: formatDateISOStringToDate(value, timeZone, dateFormat)
: '';
return formattedDate;

View File

@ -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 formatDateTimeString = ({
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;
};

View File

@ -1,7 +1,7 @@
import { GraphQLISODateTime } from '@nestjs/graphql';
import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql';
import { capitalize } from 'twenty-shared';
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@ -75,24 +75,25 @@ export const getAvailableAggregationsFromObjectFields = (
aggregateOperation: AGGREGATE_OPERATIONS.percentageNotEmpty,
};
switch (field.type) {
case FieldMetadataType.DATE_TIME:
acc[`min${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Earliest date contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
if (isFieldMetadataDateKind(field.type)) {
acc[`min${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Earliest date contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
acc[`max${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Latest date contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
break;
acc[`max${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Latest date contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
}
switch (field.type) {
case FieldMetadataType.NUMBER:
acc[`min${capitalize(field.name)}`] = {
type: GraphQLFloat,

View File

@ -1,5 +1,6 @@
export * from './constants/TwentyCompaniesBaseUrl';
export * from './constants/TwentyIconsBaseUrl';
export * from './utils/fieldMetadata/isFieldMetadataDateKind';
export * from './utils/image/getImageAbsoluteURI';
export * from './utils/strings';

View File

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