Skip to content

Commit

Permalink
Include Date fields in aggregate operations on dates (#9479)
Browse files Browse the repository at this point in the history
Follow-up on https://github.com/twentyhq/twenty/pull/9444/files - I had
forgotten to include Date field types (in addition to DateTime)
  • Loading branch information
ijreilly authored Jan 9, 2025
1 parent efb2a59 commit c535d21
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -87,7 +88,7 @@ export const computeAggregateValueAndLabel = ({

case FieldMetadataType.DateTime: {
value = aggregateValue as string;
value = formatDateString({
value = formatDateTimeString({
value,
displayAsRelativeDate,
timeZone,
Expand All @@ -96,6 +97,17 @@ export const computeAggregateValueAndLabel = ({
});
break;
}

case FieldMetadataType.Date: {
value = aggregateValue as string;
value = formatDateString({
value,
displayAsRelativeDate,
timeZone,
dateFormat,
});
break;
}
}
}
const convertedAggregateOperation =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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)}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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>;
};
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -14,7 +14,7 @@ export const DateTimeDisplay = ({
}: DateTimeDisplayProps) => {
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);

const formattedDate = formatDateString({
const formattedDate = formatDateTimeString({
value,
displayAsRelativeDate,
timeZone,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
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', () => {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
7 changes: 2 additions & 5 deletions packages/twenty-front/src/utils/string/formatDateString.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
26 changes: 26 additions & 0 deletions packages/twenty-front/src/utils/string/formatDateTimeString.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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,
};
}

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;
switch (field.type) {
case FieldMetadataType.NUMBER:
acc[`min${capitalize(field.name)}`] = {
type: GraphQLFloat,
Expand Down
1 change: 1 addition & 0 deletions packages/twenty-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Original file line number Diff line number Diff line change
@@ -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
);
};

0 comments on commit c535d21

Please sign in to comment.