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:
@ -13,6 +13,7 @@ import { formatAmount } from '~/utils/format/formatAmount';
|
|||||||
import { formatNumber } from '~/utils/format/number';
|
import { formatNumber } from '~/utils/format/number';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { formatDateString } from '~/utils/string/formatDateString';
|
import { formatDateString } from '~/utils/string/formatDateString';
|
||||||
|
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||||
|
|
||||||
export const computeAggregateValueAndLabel = ({
|
export const computeAggregateValueAndLabel = ({
|
||||||
data,
|
data,
|
||||||
@ -87,7 +88,7 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
|
|
||||||
case FieldMetadataType.DateTime: {
|
case FieldMetadataType.DateTime: {
|
||||||
value = aggregateValue as string;
|
value = aggregateValue as string;
|
||||||
value = formatDateString({
|
value = formatDateTimeString({
|
||||||
value,
|
value,
|
||||||
displayAsRelativeDate,
|
displayAsRelativeDate,
|
||||||
timeZone,
|
timeZone,
|
||||||
@ -96,6 +97,17 @@ export const computeAggregateValueAndLabel = ({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case FieldMetadataType.Date: {
|
||||||
|
value = aggregateValue as string;
|
||||||
|
value = formatDateString({
|
||||||
|
value,
|
||||||
|
displayAsRelativeDate,
|
||||||
|
timeZone,
|
||||||
|
dateFormat,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const convertedAggregateOperation =
|
const convertedAggregateOperation =
|
||||||
|
|||||||
@ -6,11 +6,13 @@ export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = {
|
|||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
FieldMetadataType.DateTime,
|
FieldMetadataType.DateTime,
|
||||||
|
FieldMetadataType.Date,
|
||||||
],
|
],
|
||||||
[AGGREGATE_OPERATIONS.max]: [
|
[AGGREGATE_OPERATIONS.max]: [
|
||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
FieldMetadataType.DateTime,
|
FieldMetadataType.DateTime,
|
||||||
|
FieldMetadataType.Date,
|
||||||
],
|
],
|
||||||
[AGGREGATE_OPERATIONS.avg]: [
|
[AGGREGATE_OPERATIONS.avg]: [
|
||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||||
|
import { isFieldMetadataDateKind } from 'twenty-shared';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const convertAggregateOperationToExtendedAggregateOperation = (
|
export const convertAggregateOperationToExtendedAggregateOperation = (
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS,
|
aggregateOperation: AGGREGATE_OPERATIONS,
|
||||||
fieldType?: FieldMetadataType,
|
fieldType?: FieldMetadataType,
|
||||||
): ExtendedAggregateOperations => {
|
): ExtendedAggregateOperations => {
|
||||||
if (fieldType === FieldMetadataType.DateTime) {
|
if (isFieldMetadataDateKind(fieldType) === true) {
|
||||||
if (aggregateOperation === AGGREGATE_OPERATIONS.min) {
|
if (aggregateOperation === AGGREGATE_OPERATIONS.min) {
|
||||||
return 'EARLIEST';
|
return 'EARLIEST';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||||
import { capitalize } from 'twenty-shared';
|
import { capitalize, isFieldMetadataDateKind } from 'twenty-shared';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
type NameForAggregation = {
|
type NameForAggregation = {
|
||||||
@ -55,7 +55,7 @@ export const getAvailableAggregationsFromObjectFields = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === FieldMetadataType.DateTime) {
|
if (isFieldMetadataDateKind(field.type) === true) {
|
||||||
acc[field.name] = {
|
acc[field.name] = {
|
||||||
...acc[field.name],
|
...acc[field.name],
|
||||||
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,
|
[AGGREGATE_OPERATIONS.min]: `min${capitalize(field.name)}`,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
|
|
||||||
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
|
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { formatDateString } from '~/utils/string/formatDateString';
|
||||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||||
|
|
||||||
type DateDisplayProps = {
|
type DateDisplayProps = {
|
||||||
@ -15,11 +14,12 @@ export const DateDisplay = ({
|
|||||||
}: DateDisplayProps) => {
|
}: DateDisplayProps) => {
|
||||||
const { dateFormat, timeZone } = useContext(UserContext);
|
const { dateFormat, timeZone } = useContext(UserContext);
|
||||||
|
|
||||||
const formattedDate = value
|
const formattedDate = formatDateString({
|
||||||
? displayAsRelativeDate
|
value,
|
||||||
? formatDateISOStringToRelativeDate(value, true)
|
timeZone,
|
||||||
: formatDateISOStringToDate(value, timeZone, dateFormat)
|
dateFormat,
|
||||||
: '';
|
displayAsRelativeDate,
|
||||||
|
});
|
||||||
|
|
||||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { formatDateString } from '~/utils/string/formatDateString';
|
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
|
||||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||||
|
|
||||||
type DateTimeDisplayProps = {
|
type DateTimeDisplayProps = {
|
||||||
@ -14,7 +14,7 @@ export const DateTimeDisplay = ({
|
|||||||
}: DateTimeDisplayProps) => {
|
}: DateTimeDisplayProps) => {
|
||||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||||
|
|
||||||
const formattedDate = formatDateString({
|
const formattedDate = formatDateTimeString({
|
||||||
value,
|
value,
|
||||||
displayAsRelativeDate,
|
displayAsRelativeDate,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { formatDateString } from '~/utils/string/formatDateString';
|
import { formatDateString } from '~/utils/string/formatDateString';
|
||||||
|
|
||||||
@ -7,7 +6,6 @@ describe('formatDateString', () => {
|
|||||||
const defaultParams = {
|
const defaultParams = {
|
||||||
timeZone: 'UTC',
|
timeZone: 'UTC',
|
||||||
dateFormat: DateFormat.DAY_FIRST,
|
dateFormat: DateFormat.DAY_FIRST,
|
||||||
timeFormat: TimeFormat.HOUR_24,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should return empty string for null value', () => {
|
it('should return empty string for null value', () => {
|
||||||
@ -49,7 +47,7 @@ describe('formatDateString', () => {
|
|||||||
|
|
||||||
it('should format date as datetime when displayAsRelativeDate is false', () => {
|
it('should format date as datetime when displayAsRelativeDate is false', () => {
|
||||||
const mockDate = '2023-01-01T12:00:00Z';
|
const mockDate = '2023-01-01T12:00:00Z';
|
||||||
const mockFormattedDate = '1 Jan, 2023 12:00';
|
const mockFormattedDate = '1 Jan, 2023';
|
||||||
|
|
||||||
jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({
|
jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({
|
||||||
formatDateISOStringToDateTime: jest
|
formatDateISOStringToDateTime: jest
|
||||||
@ -68,7 +66,7 @@ describe('formatDateString', () => {
|
|||||||
|
|
||||||
it('should format date as datetime by default when displayAsRelativeDate is not provided', () => {
|
it('should format date as datetime by default when displayAsRelativeDate is not provided', () => {
|
||||||
const mockDate = '2023-01-01T12:00:00Z';
|
const mockDate = '2023-01-01T12:00:00Z';
|
||||||
const mockFormattedDate = '1 Jan, 2023 12:00';
|
const mockFormattedDate = '1 Jan, 2023';
|
||||||
|
|
||||||
jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({
|
jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({
|
||||||
formatDateISOStringToDateTime: jest
|
formatDateISOStringToDateTime: jest
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,25 +1,22 @@
|
|||||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
|
||||||
import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime';
|
|
||||||
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
|
import { formatDateISOStringToRelativeDate } from '@/localization/utils/formatDateISOStringToRelativeDate';
|
||||||
|
|
||||||
export const formatDateString = ({
|
export const formatDateString = ({
|
||||||
value,
|
value,
|
||||||
timeZone,
|
timeZone,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
timeFormat,
|
|
||||||
displayAsRelativeDate,
|
displayAsRelativeDate,
|
||||||
}: {
|
}: {
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
dateFormat: DateFormat;
|
dateFormat: DateFormat;
|
||||||
timeFormat: TimeFormat;
|
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
displayAsRelativeDate?: boolean;
|
displayAsRelativeDate?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const formattedDate = value
|
const formattedDate = value
|
||||||
? displayAsRelativeDate
|
? displayAsRelativeDate
|
||||||
? formatDateISOStringToRelativeDate(value)
|
? formatDateISOStringToRelativeDate(value)
|
||||||
: formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
|
: formatDateISOStringToDate(value, timeZone, dateFormat)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { GraphQLISODateTime } from '@nestjs/graphql';
|
import { GraphQLISODateTime } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from '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';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
@ -75,24 +75,25 @@ export const getAvailableAggregationsFromObjectFields = (
|
|||||||
aggregateOperation: AGGREGATE_OPERATIONS.percentageNotEmpty,
|
aggregateOperation: AGGREGATE_OPERATIONS.percentageNotEmpty,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (field.type) {
|
if (isFieldMetadataDateKind(field.type)) {
|
||||||
case FieldMetadataType.DATE_TIME:
|
acc[`min${capitalize(field.name)}`] = {
|
||||||
acc[`min${capitalize(field.name)}`] = {
|
type: GraphQLISODateTime,
|
||||||
type: GraphQLISODateTime,
|
description: `Earliest date contained in the field ${field.name}`,
|
||||||
description: `Earliest date contained in the field ${field.name}`,
|
fromField: field.name,
|
||||||
fromField: field.name,
|
fromFieldType: field.type,
|
||||||
fromFieldType: field.type,
|
aggregateOperation: AGGREGATE_OPERATIONS.min,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.min,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
acc[`max${capitalize(field.name)}`] = {
|
acc[`max${capitalize(field.name)}`] = {
|
||||||
type: GraphQLISODateTime,
|
type: GraphQLISODateTime,
|
||||||
description: `Latest date contained in the field ${field.name}`,
|
description: `Latest date contained in the field ${field.name}`,
|
||||||
fromField: field.name,
|
fromField: field.name,
|
||||||
fromFieldType: field.type,
|
fromFieldType: field.type,
|
||||||
aggregateOperation: AGGREGATE_OPERATIONS.max,
|
aggregateOperation: AGGREGATE_OPERATIONS.max,
|
||||||
};
|
};
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
switch (field.type) {
|
||||||
case FieldMetadataType.NUMBER:
|
case FieldMetadataType.NUMBER:
|
||||||
acc[`min${capitalize(field.name)}`] = {
|
acc[`min${capitalize(field.name)}`] = {
|
||||||
type: GraphQLFloat,
|
type: GraphQLFloat,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export * from './constants/TwentyCompaniesBaseUrl';
|
export * from './constants/TwentyCompaniesBaseUrl';
|
||||||
export * from './constants/TwentyIconsBaseUrl';
|
export * from './constants/TwentyIconsBaseUrl';
|
||||||
|
export * from './utils/fieldMetadata/isFieldMetadataDateKind';
|
||||||
export * from './utils/image/getImageAbsoluteURI';
|
export * from './utils/image/getImageAbsoluteURI';
|
||||||
export * from './utils/strings';
|
export * from './utils/strings';
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user