unlock relation date fields on dashboards (#16207)
This commit is contained in:
+2
@@ -96,6 +96,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
|
||||
isGroupByEnabled as boolean,
|
||||
configuration,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
),
|
||||
)
|
||||
.map((item) => item.id),
|
||||
@@ -144,6 +145,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
|
||||
isGroupByEnabled as boolean,
|
||||
configuration,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
+2
-6
@@ -16,7 +16,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
@@ -70,11 +70,7 @@ export const ChartGroupByFieldSelectionRelationFieldView = ({
|
||||
|
||||
return filterBySearchQuery({
|
||||
items: targetObjectMetadataItem.fields.filter(
|
||||
(field) =>
|
||||
!field.isSystem &&
|
||||
!isFieldRelation(field) &&
|
||||
// TODO: Backend doesn't fully support date fields for relation fields yet so we hide them for now. https://github.com/twentyhq/core-team-issues/issues/1935
|
||||
!isFieldMetadataDateKind(field.type),
|
||||
(field) => !field.isSystem && !isFieldRelation(field),
|
||||
),
|
||||
searchQuery,
|
||||
getSearchableValues: (field) => [field.label, field.name],
|
||||
|
||||
+46
@@ -385,6 +385,28 @@ describe('shouldHideChartSetting', () => {
|
||||
});
|
||||
|
||||
describe('DATE_GRANULARITY (Pie Chart)', () => {
|
||||
const relationField: any = {
|
||||
id: 'relation-field-id',
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
relation: { targetObjectMetadata: { nameSingular: 'company' } },
|
||||
};
|
||||
|
||||
const targetObjectMetadata: any = {
|
||||
id: 'company-id',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fields: [
|
||||
{
|
||||
id: 'company-created-at',
|
||||
name: 'createdAt',
|
||||
label: 'Created At',
|
||||
type: FieldMetadataType.DATE,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('should show when group by field is a date field', () => {
|
||||
const pieChartConfig: ChartConfiguration = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
@@ -435,6 +457,30 @@ describe('shouldHideChartSetting', () => {
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should show when group by field is a relation date subfield', () => {
|
||||
const objectMetadataItemWithRelation: ObjectMetadataItem = {
|
||||
...mockObjectMetadataItem,
|
||||
fields: [...mockObjectMetadataItem.fields, relationField],
|
||||
} as any;
|
||||
|
||||
const pieChartConfig: ChartConfiguration = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
groupByFieldMetadataId: relationField.id,
|
||||
groupBySubFieldName: 'createdAt',
|
||||
} as any;
|
||||
|
||||
const result = shouldHideChartSetting(
|
||||
mockDateGranularityItem,
|
||||
'object-id',
|
||||
true,
|
||||
pieChartConfig,
|
||||
objectMetadataItemWithRelation,
|
||||
[objectMetadataItemWithRelation, targetObjectMetadata] as any,
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+12
-2
@@ -3,12 +3,15 @@ import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layou
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
|
||||
const shouldHideDateGranularityBasedOnFieldType = (
|
||||
fieldMetadataId: string | undefined | null,
|
||||
subFieldName: string | undefined | null,
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
): boolean => {
|
||||
if (!isDefined(fieldMetadataId)) {
|
||||
return true;
|
||||
@@ -22,8 +25,8 @@ const shouldHideDateGranularityBasedOnFieldType = (
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isFieldRelation(field)) {
|
||||
return true;
|
||||
if (isFieldRelation(field) && isDefined(subFieldName)) {
|
||||
return !isNestedFieldDateType(field, subFieldName, objectMetadataItems);
|
||||
}
|
||||
|
||||
return !isFieldMetadataDateKind(field.type);
|
||||
@@ -35,6 +38,7 @@ export const shouldHideChartSetting = (
|
||||
isGroupByEnabled: boolean,
|
||||
configuration?: ChartConfiguration,
|
||||
objectMetadataItem?: ObjectMetadataItem,
|
||||
objectMetadataItems?: ObjectMetadataItem[],
|
||||
): boolean => {
|
||||
const hasNoObjectMetadata = !isNonEmptyString(objectMetadataId);
|
||||
const dependsOnSource = item?.dependsOn?.includes(
|
||||
@@ -53,7 +57,9 @@ export const shouldHideChartSetting = (
|
||||
if (isBarOrLineChart) {
|
||||
return shouldHideDateGranularityBasedOnFieldType(
|
||||
configuration.primaryAxisGroupByFieldMetadataId,
|
||||
configuration.primaryAxisGroupBySubFieldName,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +72,9 @@ export const shouldHideChartSetting = (
|
||||
if (isBarOrLineChart) {
|
||||
return shouldHideDateGranularityBasedOnFieldType(
|
||||
configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
configuration.secondaryAxisGroupBySubFieldName,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +83,9 @@ export const shouldHideChartSetting = (
|
||||
if (configuration.__typename === 'PieChartConfiguration') {
|
||||
return shouldHideDateGranularityBasedOnFieldType(
|
||||
configuration.groupByFieldMetadataId,
|
||||
configuration.groupBySubFieldName,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { transformGroupByDataToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
import { AxisNameDisplay } from '~/generated/graphql';
|
||||
|
||||
jest.mock(
|
||||
'@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData',
|
||||
() => ({
|
||||
fillDateGapsInBarChartData: jest.fn(({ data }) => ({
|
||||
data,
|
||||
wasTruncated: true,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData',
|
||||
() => ({
|
||||
transformOneDimensionalGroupByToBarChartData: jest.fn(() => ({
|
||||
data: [],
|
||||
indexBy: 'x',
|
||||
keys: ['value'],
|
||||
series: [],
|
||||
hasTooManyGroups: false,
|
||||
formattedToRawLookup: new Map(),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@/page-layout/widgets/graph/utils/filterGroupByResults', () => ({
|
||||
filterGroupByResults: jest.fn((args) => args.rawResults),
|
||||
}));
|
||||
|
||||
const { fillDateGapsInBarChartData } = jest.requireMock(
|
||||
'@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData',
|
||||
) as { fillDateGapsInBarChartData: jest.Mock };
|
||||
|
||||
describe('transformGroupByDataToBarChartData', () => {
|
||||
it('fills date gaps when grouping by a relation date subfield with granularity', () => {
|
||||
const groupByField = {
|
||||
id: 'group-by-field',
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
};
|
||||
|
||||
const aggregateField = {
|
||||
id: 'aggregate-field',
|
||||
name: 'count',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
};
|
||||
|
||||
const objectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fields: [groupByField, aggregateField],
|
||||
} as any;
|
||||
|
||||
const configuration = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
aggregateFieldMetadataId: aggregateField.id,
|
||||
aggregateOperation: 'COUNT',
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: groupByField.id,
|
||||
primaryAxisGroupBySubFieldName: 'createdAt',
|
||||
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
} as any;
|
||||
|
||||
const groupByData = {
|
||||
companiesGroupBy: [
|
||||
{ groupByDimensionValues: ['2024-01-01T00:00:00.000Z'], COUNT: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
fillDateGapsInBarChartData.mockClear();
|
||||
|
||||
const result = transformGroupByDataToBarChartData({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
aggregateOperation: 'COUNT',
|
||||
});
|
||||
|
||||
expect(fillDateGapsInBarChartData).toHaveBeenCalledTimes(1);
|
||||
expect(fillDateGapsInBarChartData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
|
||||
hasSecondDimension: false,
|
||||
}),
|
||||
);
|
||||
expect(result.hasTooManyGroups).toBe(true);
|
||||
});
|
||||
});
|
||||
+4
-1
@@ -150,11 +150,14 @@ export const transformGroupByDataToBarChartData = ({
|
||||
const showLegend = configuration.displayLegend ?? true;
|
||||
|
||||
const isDateField = isFieldMetadataDateKind(groupByFieldX.type);
|
||||
const isNestedDateField =
|
||||
!isDateField && isDefined(configuration.primaryAxisDateGranularity);
|
||||
const shouldApplyDateGapFill = isDateField || isNestedDateField;
|
||||
|
||||
const omitNullValues = configuration.omitNullValues ?? false;
|
||||
|
||||
const dateGapFillResult =
|
||||
isDateField && !omitNullValues
|
||||
shouldApplyDateGapFill && !omitNullValues
|
||||
? fillDateGapsInBarChartData({
|
||||
data: filteredResults,
|
||||
keys: [aggregateField.name],
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant';
|
||||
import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant';
|
||||
import { transformGroupByDataToPieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData';
|
||||
import {
|
||||
AggregateOperations,
|
||||
FieldMetadataType,
|
||||
GraphType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type PieChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
jest.mock(
|
||||
'@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues',
|
||||
() => ({
|
||||
formatPrimaryDimensionValues: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
const { formatPrimaryDimensionValues } = jest.requireMock(
|
||||
'@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues',
|
||||
) as { formatPrimaryDimensionValues: jest.Mock };
|
||||
|
||||
describe('transformGroupByDataToPieChartData', () => {
|
||||
it('keeps null group buckets aligned with their aggregate values', () => {
|
||||
const groupByField = {
|
||||
id: 'group-by-field',
|
||||
name: 'status',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Status',
|
||||
};
|
||||
|
||||
const aggregateField = {
|
||||
id: 'aggregate-field',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Id',
|
||||
};
|
||||
|
||||
const objectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fields: [groupByField, aggregateField],
|
||||
} as any;
|
||||
|
||||
const configuration = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
aggregateFieldMetadataId: aggregateField.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
graphType: GraphType.PIE,
|
||||
groupByFieldMetadataId: groupByField.id,
|
||||
displayLegend: true,
|
||||
} as PieChartConfiguration;
|
||||
|
||||
const groupByData = {
|
||||
companiesGroupBy: [
|
||||
{ groupByDimensionValues: [null], COUNT: 2 },
|
||||
{ groupByDimensionValues: ['Active'], COUNT: 5 },
|
||||
],
|
||||
};
|
||||
|
||||
formatPrimaryDimensionValues.mockReturnValue([
|
||||
{
|
||||
formattedPrimaryDimensionValue: 'Not Set',
|
||||
rawPrimaryDimensionValue: null,
|
||||
},
|
||||
{
|
||||
formattedPrimaryDimensionValue: 'Active',
|
||||
rawPrimaryDimensionValue: 'Active',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = transformGroupByDataToPieChartData({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
aggregateOperation: 'COUNT',
|
||||
});
|
||||
|
||||
expect(result.data).toEqual([
|
||||
{ id: 'Not Set', value: 2, color: GRAPH_DEFAULT_COLOR },
|
||||
{ id: 'Active', value: 5, color: GRAPH_DEFAULT_COLOR },
|
||||
]);
|
||||
expect(result.formattedToRawLookup.get('Not Set')).toBeNull();
|
||||
expect(result.formattedToRawLookup.get('Active')).toBe('Active');
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('honors color configuration, hides legend, and flags too many groups', () => {
|
||||
const groupByField = {
|
||||
id: 'group-by-field',
|
||||
name: 'status',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Status',
|
||||
};
|
||||
|
||||
const aggregateField = {
|
||||
id: 'aggregate-field',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Id',
|
||||
};
|
||||
|
||||
const objectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fields: [groupByField, aggregateField],
|
||||
} as any;
|
||||
|
||||
const configuration = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
aggregateFieldMetadataId: aggregateField.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
graphType: GraphType.PIE,
|
||||
groupByFieldMetadataId: groupByField.id,
|
||||
displayLegend: false,
|
||||
color: 'red',
|
||||
} as PieChartConfiguration;
|
||||
|
||||
const totalResults = PIE_CHART_MAXIMUM_NUMBER_OF_SLICES + 1;
|
||||
const groupByData = {
|
||||
companiesGroupBy: Array.from({ length: totalResults }).map(
|
||||
(_unused, index) => ({
|
||||
groupByDimensionValues: [`Group ${index}`],
|
||||
COUNT: index + 1,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
formatPrimaryDimensionValues.mockReturnValue(
|
||||
Array.from({ length: totalResults }).map((_unused, index) => ({
|
||||
formattedPrimaryDimensionValue: `Group ${index}`,
|
||||
rawPrimaryDimensionValue: `Group ${index}`,
|
||||
})),
|
||||
);
|
||||
|
||||
const result = transformGroupByDataToPieChartData({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
aggregateOperation: 'COUNT',
|
||||
});
|
||||
|
||||
expect(result.showLegend).toBe(false);
|
||||
expect(result.hasTooManyGroups).toBe(true);
|
||||
expect(result.data).toHaveLength(PIE_CHART_MAXIMUM_NUMBER_OF_SLICES);
|
||||
expect(result.data[0]).toEqual({
|
||||
id: 'Group 0',
|
||||
value: 1,
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
@@ -1,5 +1,6 @@
|
||||
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { generateGroupByAggregateQuery } from '@/object-record/record-aggregate/utils/generateGroupByAggregateQuery';
|
||||
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
|
||||
import { useGraphWidgetQueryCommon } from '@/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon';
|
||||
@@ -33,6 +34,8 @@ export const useGraphWidgetGroupByQuery = ({
|
||||
configuration,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
if (!isDefined(aggregateField)) {
|
||||
throw new Error('Aggregate field not found');
|
||||
}
|
||||
@@ -63,6 +66,7 @@ export const useGraphWidgetGroupByQuery = ({
|
||||
const groupByQueryVariables = isPieChart(configuration)
|
||||
? generateGroupByQueryVariablesFromPieChartConfiguration({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
chartConfiguration: configuration,
|
||||
aggregateOperation: aggregateOperation,
|
||||
limit,
|
||||
@@ -70,6 +74,7 @@ export const useGraphWidgetGroupByQuery = ({
|
||||
})
|
||||
: generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
chartConfiguration: configuration as
|
||||
| BarChartConfiguration
|
||||
| LineChartConfiguration,
|
||||
|
||||
+20
@@ -42,6 +42,26 @@ describe('buildGroupByFieldObject', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return date granularity for relation date field', () => {
|
||||
const field = {
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
} as any;
|
||||
|
||||
const result = buildGroupByFieldObject({
|
||||
field,
|
||||
subFieldName: 'createdAt',
|
||||
dateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
|
||||
isNestedDateField: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
company: {
|
||||
createdAt: { granularity: ObjectRecordGroupByDateGranularity.MONTH },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return nested object for composite fields with subfield', () => {
|
||||
const field = {
|
||||
name: 'name',
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
jest.mock('@/page-layout/widgets/graph/utils/formatDimensionValue', () => ({
|
||||
formatDimensionValue: jest.fn((args: { value: unknown }) => {
|
||||
return `formatted-${String(args.value)}`;
|
||||
}),
|
||||
}));
|
||||
|
||||
const { formatDimensionValue } = jest.requireMock(
|
||||
'@/page-layout/widgets/graph/utils/formatDimensionValue',
|
||||
) as { formatDimensionValue: jest.Mock };
|
||||
|
||||
describe('formatPrimaryDimensionValues', () => {
|
||||
it('includes buckets where the primary dimension value is null', () => {
|
||||
const result = formatPrimaryDimensionValues({
|
||||
groupByRawResults: [
|
||||
{ groupByDimensionValues: [null] },
|
||||
{ groupByDimensionValues: ['Active'] },
|
||||
],
|
||||
primaryAxisGroupByField: {
|
||||
name: 'status',
|
||||
type: FieldMetadataType.TEXT,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].rawPrimaryDimensionValue).toBeNull();
|
||||
expect(result[0].formattedPrimaryDimensionValue).toBe('formatted-null');
|
||||
expect(result[1].formattedPrimaryDimensionValue).toBe('formatted-Active');
|
||||
});
|
||||
|
||||
it('passes granularity and subfield to the formatter', () => {
|
||||
formatDimensionValue.mockClear();
|
||||
|
||||
formatPrimaryDimensionValues({
|
||||
groupByRawResults: [
|
||||
{ groupByDimensionValues: ['2024-01-15T00:00:00.000Z'] },
|
||||
],
|
||||
primaryAxisGroupByField: {
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
} as any,
|
||||
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
|
||||
primaryAxisGroupBySubFieldName: 'createdAt',
|
||||
});
|
||||
|
||||
expect(formatDimensionValue).toHaveBeenCalledWith({
|
||||
value: '2024-01-15T00:00:00.000Z',
|
||||
fieldMetadata: expect.objectContaining({ name: 'createdAt' }),
|
||||
dateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
|
||||
subFieldName: 'createdAt',
|
||||
});
|
||||
});
|
||||
});
|
||||
+109
-1
@@ -1,6 +1,9 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { generateGroupByQueryVariablesFromBarOrLineChartConfiguration } from '@/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarOrLineChartConfiguration';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
AggregateOperations,
|
||||
type BarChartConfiguration,
|
||||
@@ -67,6 +70,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
@@ -81,6 +85,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
@@ -97,6 +102,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-4',
|
||||
@@ -111,6 +117,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-3',
|
||||
@@ -128,6 +135,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
@@ -142,6 +150,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
@@ -152,6 +161,98 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('applies date granularity when primary axis uses a relation date subfield', () => {
|
||||
const relationField = {
|
||||
id: 'field-rel',
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
relation: { targetObjectMetadata: { nameSingular: 'company' } },
|
||||
};
|
||||
|
||||
const objectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-main',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [relationField],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const targetObjectMetadata: ObjectMetadataItem = {
|
||||
id: 'obj-company',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fields: [
|
||||
{
|
||||
id: 'company-created-at',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems: [objectMetadataItem, targetObjectMetadata],
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: relationField.id,
|
||||
primaryAxisGroupBySubFieldName: 'createdAt',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.groupBy[0]).toEqual({
|
||||
company: {
|
||||
createdAt: { granularity: ObjectRecordGroupByDateGranularity.DAY },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('applies date granularity for relation date subfield in line charts as well', () => {
|
||||
const relationField = {
|
||||
id: 'field-rel',
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
relation: { targetObjectMetadata: { nameSingular: 'company' } },
|
||||
};
|
||||
|
||||
const objectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-main',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [relationField],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const targetObjectMetadata: ObjectMetadataItem = {
|
||||
id: 'obj-company',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fields: [
|
||||
{
|
||||
id: 'company-created-at',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems: [objectMetadataItem, targetObjectMetadata],
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: relationField.id,
|
||||
primaryAxisGroupBySubFieldName: 'createdAt',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.groupBy[0]).toEqual({
|
||||
company: {
|
||||
createdAt: { granularity: ObjectRecordGroupByDateGranularity.DAY },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Line Chart Configuration', () => {
|
||||
@@ -159,6 +260,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
@@ -173,6 +275,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
@@ -189,6 +292,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-3',
|
||||
@@ -204,6 +308,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-4',
|
||||
@@ -218,6 +323,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
const result =
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
@@ -234,6 +340,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
expect(() =>
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'invalid-field',
|
||||
}),
|
||||
@@ -245,6 +352,7 @@ describe('generateGroupByQueryVariablesFromBarOrLineChartConfiguration', () => {
|
||||
expect(() =>
|
||||
generateGroupByQueryVariablesFromBarOrLineChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'invalid-field',
|
||||
}),
|
||||
|
||||
+53
@@ -6,6 +6,7 @@ import {
|
||||
GraphType,
|
||||
type PieChartConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { generateGroupByQueryVariablesFromPieChartConfiguration } from '../generateGroupByQueryVariablesFromPieChartConfiguration';
|
||||
|
||||
describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => {
|
||||
@@ -53,6 +54,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => {
|
||||
it('should generate variables with single groupBy field', () => {
|
||||
const result = generateGroupByQueryVariablesFromPieChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildPieChartConfiguration({
|
||||
groupByFieldMetadataId: 'field-1',
|
||||
groupBySubFieldName: null,
|
||||
@@ -65,6 +67,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => {
|
||||
it('should generate variables with composite field', () => {
|
||||
const result = generateGroupByQueryVariablesFromPieChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildPieChartConfiguration({
|
||||
groupByFieldMetadataId: 'field-4',
|
||||
groupBySubFieldName: 'firstName',
|
||||
@@ -77,6 +80,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => {
|
||||
it('should generate variables with date field and granularity', () => {
|
||||
const result = generateGroupByQueryVariablesFromPieChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildPieChartConfiguration({
|
||||
groupByFieldMetadataId: 'field-3',
|
||||
groupBySubFieldName: null,
|
||||
@@ -90,6 +94,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => {
|
||||
it('should generate variables with limit', () => {
|
||||
const result = generateGroupByQueryVariablesFromPieChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildPieChartConfiguration({
|
||||
groupByFieldMetadataId: 'field-1',
|
||||
}),
|
||||
@@ -103,6 +108,7 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => {
|
||||
it('should generate variables with orderBy', () => {
|
||||
const result = generateGroupByQueryVariablesFromPieChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildPieChartConfiguration({
|
||||
groupByFieldMetadataId: 'field-1',
|
||||
orderBy: GraphOrderBy.VALUE_ASC,
|
||||
@@ -115,11 +121,58 @@ describe('generateGroupByQueryVariablesFromPieChartConfiguration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Relation date subfield', () => {
|
||||
it('applies date granularity when grouping by a relation date subfield', () => {
|
||||
const relationField = {
|
||||
id: 'field-rel',
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
relation: { targetObjectMetadata: { nameSingular: 'company' } },
|
||||
};
|
||||
|
||||
const mainObjectMetadata: ObjectMetadataItem = {
|
||||
id: 'obj-main',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [relationField],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const targetObjectMetadata: ObjectMetadataItem = {
|
||||
id: 'obj-company',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fields: [
|
||||
{
|
||||
id: 'company-created-at',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const result = generateGroupByQueryVariablesFromPieChartConfiguration({
|
||||
objectMetadataItem: mainObjectMetadata,
|
||||
objectMetadataItems: [mainObjectMetadata, targetObjectMetadata],
|
||||
chartConfiguration: buildPieChartConfiguration({
|
||||
groupByFieldMetadataId: relationField.id,
|
||||
groupBySubFieldName: 'createdAt',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.groupBy[0]).toEqual({
|
||||
company: {
|
||||
createdAt: { granularity: ObjectRecordGroupByDateGranularity.DAY },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should throw error when groupBy field not found', () => {
|
||||
expect(() =>
|
||||
generateGroupByQueryVariablesFromPieChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
objectMetadataItems: [],
|
||||
chartConfiguration: buildPieChartConfiguration({
|
||||
groupByFieldMetadataId: 'invalid-field',
|
||||
}),
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { getRelationFieldOrderBy } from '@/page-layout/widgets/graph/utils/getRelationFieldOrderBy';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
OrderByDirection,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
describe('getRelationFieldOrderBy', () => {
|
||||
const relationField = {
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
} as any;
|
||||
|
||||
it('returns relation id ordering when no subfield provided', () => {
|
||||
const orderBy = getRelationFieldOrderBy(
|
||||
relationField,
|
||||
null,
|
||||
OrderByDirection.AscNullsLast,
|
||||
);
|
||||
|
||||
expect(orderBy).toEqual({ companyId: OrderByDirection.AscNullsLast });
|
||||
});
|
||||
|
||||
it('adds granularity when nested relation field is a date field', () => {
|
||||
const orderBy = getRelationFieldOrderBy(
|
||||
relationField,
|
||||
'createdAt',
|
||||
OrderByDirection.DescNullsLast,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(orderBy).toEqual({
|
||||
company: {
|
||||
createdAt: {
|
||||
orderBy: OrderByDirection.DescNullsLast,
|
||||
granularity: ObjectRecordGroupByDateGranularity.DAY,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns nested relation ordering without granularity for non-date subfield', () => {
|
||||
const orderBy = getRelationFieldOrderBy(
|
||||
relationField,
|
||||
'name',
|
||||
OrderByDirection.AscNullsLast,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(orderBy).toEqual({
|
||||
company: {
|
||||
name: OrderByDirection.AscNullsLast,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
describe('isNestedFieldDateType', () => {
|
||||
const companyObject = {
|
||||
id: 'company-id',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
fields: [
|
||||
{ id: 'createdAt', name: 'createdAt', type: FieldMetadataType.DATE_TIME },
|
||||
{ id: 'name', name: 'name', type: FieldMetadataType.TEXT },
|
||||
],
|
||||
} as any;
|
||||
|
||||
const relationField = {
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
relation: { targetObjectMetadata: { nameSingular: 'company' } },
|
||||
} as any;
|
||||
|
||||
it('returns true for a relation subfield that is a date type', () => {
|
||||
const result = isNestedFieldDateType(relationField, 'createdAt', [
|
||||
companyObject,
|
||||
]);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the nested subfield is not a date type', () => {
|
||||
const result = isNestedFieldDateType(relationField, 'name', [
|
||||
companyObject,
|
||||
]);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when subFieldName is missing', () => {
|
||||
const result = isNestedFieldDateType(relationField, undefined, [
|
||||
companyObject,
|
||||
]);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-relation fields', () => {
|
||||
const nonRelationField = {
|
||||
name: 'status',
|
||||
type: FieldMetadataType.TEXT,
|
||||
} as any;
|
||||
|
||||
const result = isNestedFieldDateType(nonRelationField, 'createdAt', [
|
||||
companyObject,
|
||||
]);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+12
@@ -20,11 +20,13 @@ export const buildGroupByFieldObject = ({
|
||||
subFieldName,
|
||||
dateGranularity,
|
||||
firstDayOfTheWeek,
|
||||
isNestedDateField,
|
||||
}: {
|
||||
field: FieldMetadataItem;
|
||||
subFieldName?: string | null;
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity;
|
||||
firstDayOfTheWeek?: number | null;
|
||||
isNestedDateField?: boolean;
|
||||
}): GroupByFieldObject => {
|
||||
const isRelation = isFieldRelation(field) || isFieldMorphRelation(field);
|
||||
const isComposite = isCompositeFieldType(field.type);
|
||||
@@ -39,6 +41,16 @@ export const buildGroupByFieldObject = ({
|
||||
const nestedFieldName = parts[0];
|
||||
const nestedSubFieldName = parts[1];
|
||||
|
||||
if (isNestedDateField === true || isDefined(dateGranularity)) {
|
||||
return {
|
||||
[field.name]: {
|
||||
[nestedFieldName]: {
|
||||
granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isDefined(nestedSubFieldName)) {
|
||||
return {
|
||||
[field.name]: {
|
||||
|
||||
+20
@@ -88,6 +88,26 @@ export const formatDimensionValue = ({
|
||||
return formatDateByGranularity(new Date(String(value)), dateGranularity);
|
||||
}
|
||||
|
||||
case FieldMetadataType.RELATION: {
|
||||
if (isDefined(dateGranularity)) {
|
||||
if (
|
||||
dateGranularity ===
|
||||
ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK ||
|
||||
dateGranularity ===
|
||||
ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR ||
|
||||
dateGranularity ===
|
||||
ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR
|
||||
) {
|
||||
return String(value);
|
||||
}
|
||||
return formatDateByGranularity(
|
||||
new Date(String(value)),
|
||||
dateGranularity,
|
||||
);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.CURRENCY: {
|
||||
if (
|
||||
|
||||
+2
-7
@@ -2,7 +2,6 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ObjectRecordGroupByDateGranularity } from '~/generated/graphql';
|
||||
|
||||
type FormatPrimaryDimensionValuesParameters = {
|
||||
@@ -27,12 +26,8 @@ export const formatPrimaryDimensionValues = ({
|
||||
(accumulator, rawResult) => {
|
||||
const groupByDimensionValues = rawResult.groupByDimensionValues;
|
||||
|
||||
if (!isDefined(groupByDimensionValues?.[0])) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
const rawPrimaryDimensionValue =
|
||||
groupByDimensionValues[0] as RawDimensionValue;
|
||||
const rawPrimaryDimensionValue = (groupByDimensionValues?.[0] ??
|
||||
null) as RawDimensionValue;
|
||||
|
||||
const formattedPrimaryDimensionValue = formatDimensionValue({
|
||||
value: rawPrimaryDimensionValue,
|
||||
|
||||
+34
-5
@@ -1,6 +1,7 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
|
||||
import { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType';
|
||||
import {
|
||||
type AggregateOrderByWithGroupByField,
|
||||
type ObjectRecordOrderByForCompositeField,
|
||||
@@ -20,12 +21,14 @@ import {
|
||||
|
||||
export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
chartConfiguration,
|
||||
aggregateOperation,
|
||||
limit,
|
||||
firstDayOfTheWeek,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
chartConfiguration: BarChartConfiguration | LineChartConfiguration;
|
||||
aggregateOperation?: string;
|
||||
limit?: number;
|
||||
@@ -58,33 +61,50 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
|
||||
|
||||
const isFieldXDate = isFieldMetadataDateKind(groupByFieldX.type);
|
||||
|
||||
const isFieldXNestedDate = isNestedFieldDateType(
|
||||
groupByFieldX,
|
||||
groupBySubFieldNameX,
|
||||
objectMetadataItems,
|
||||
);
|
||||
|
||||
const shouldApplyDateGranularityX = isFieldXDate || isFieldXNestedDate;
|
||||
|
||||
const groupBy: Array<GroupByFieldObject> = [];
|
||||
|
||||
groupBy.push(
|
||||
buildGroupByFieldObject({
|
||||
field: groupByFieldX,
|
||||
subFieldName: groupBySubFieldNameX,
|
||||
dateGranularity: isFieldXDate
|
||||
dateGranularity: shouldApplyDateGranularityX
|
||||
? (chartConfiguration.primaryAxisDateGranularity ??
|
||||
GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
|
||||
firstDayOfTheWeek,
|
||||
isNestedDateField: isFieldXNestedDate,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isDefined(groupByFieldY)) {
|
||||
const isFieldYDate = isFieldMetadataDateKind(groupByFieldY.type);
|
||||
|
||||
const isFieldYNestedDate = isNestedFieldDateType(
|
||||
groupByFieldY,
|
||||
groupBySubFieldNameY,
|
||||
objectMetadataItems,
|
||||
);
|
||||
|
||||
const shouldApplyDateGranularityY = isFieldYDate || isFieldYNestedDate;
|
||||
|
||||
groupBy.push(
|
||||
buildGroupByFieldObject({
|
||||
field: groupByFieldY,
|
||||
subFieldName: groupBySubFieldNameY,
|
||||
dateGranularity: isFieldYDate
|
||||
dateGranularity: shouldApplyDateGranularityY
|
||||
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
|
||||
GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
firstDayOfTheWeek,
|
||||
isNestedDateField: isFieldYNestedDate,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -104,7 +124,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
|
||||
groupByField: groupByFieldX,
|
||||
groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: isFieldXDate
|
||||
dateGranularity: shouldApplyDateGranularityX
|
||||
? (chartConfiguration.primaryAxisDateGranularity ??
|
||||
GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
@@ -117,6 +137,15 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
|
||||
) {
|
||||
const isFieldYDateForOrderBy = isFieldMetadataDateKind(groupByFieldY.type);
|
||||
|
||||
const isFieldYNestedDateForOrderBy = isNestedFieldDateType(
|
||||
groupByFieldY,
|
||||
groupBySubFieldNameY,
|
||||
objectMetadataItems,
|
||||
);
|
||||
|
||||
const shouldApplyDateGranularityYForOrderBy =
|
||||
isFieldYDateForOrderBy || isFieldYNestedDateForOrderBy;
|
||||
|
||||
orderBy.push(
|
||||
getGroupByOrderBy({
|
||||
graphOrderBy: chartConfiguration.secondaryAxisOrderBy,
|
||||
@@ -124,7 +153,7 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
|
||||
groupBySubFieldName:
|
||||
chartConfiguration.secondaryAxisGroupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: isFieldYDateForOrderBy
|
||||
dateGranularity: shouldApplyDateGranularityYForOrderBy
|
||||
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
|
||||
GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
|
||||
+14
-3
@@ -1,6 +1,7 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
|
||||
import { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType';
|
||||
import {
|
||||
type AggregateOrderByWithGroupByField,
|
||||
type ObjectRecordOrderByForCompositeField,
|
||||
@@ -17,12 +18,14 @@ import {
|
||||
|
||||
export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
chartConfiguration,
|
||||
aggregateOperation,
|
||||
limit,
|
||||
firstDayOfTheWeek,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
chartConfiguration: PieChartConfiguration;
|
||||
aggregateOperation?: string;
|
||||
limit?: number;
|
||||
@@ -45,15 +48,23 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
|
||||
|
||||
const isFieldDate = isFieldMetadataDateKind(groupByField.type);
|
||||
|
||||
const isNestedDate = isNestedFieldDateType(
|
||||
groupByField,
|
||||
groupBySubFieldName,
|
||||
objectMetadataItems,
|
||||
);
|
||||
|
||||
const shouldApplyDateGranularity = isFieldDate || isNestedDate;
|
||||
|
||||
const groupBy: Array<GroupByFieldObject> = [
|
||||
buildGroupByFieldObject({
|
||||
field: groupByField,
|
||||
subFieldName: groupBySubFieldName,
|
||||
|
||||
dateGranularity: isFieldDate
|
||||
dateGranularity: shouldApplyDateGranularity
|
||||
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
firstDayOfTheWeek,
|
||||
isNestedDateField: isNestedDate,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -72,7 +83,7 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
|
||||
groupByField,
|
||||
groupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: isFieldDate
|
||||
dateGranularity: shouldApplyDateGranularity
|
||||
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
}),
|
||||
|
||||
@@ -51,6 +51,7 @@ export const getFieldOrderBy = (
|
||||
groupByField,
|
||||
groupBySubFieldName,
|
||||
direction,
|
||||
dateGranularity,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -1,5 +1,7 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import {
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
type ObjectRecordOrderByForRelationField,
|
||||
type ObjectRecordOrderByForScalarField,
|
||||
type OrderByDirection,
|
||||
@@ -10,6 +12,8 @@ export const getRelationFieldOrderBy = (
|
||||
groupByField: FieldMetadataItem,
|
||||
groupBySubFieldName: string | null | undefined,
|
||||
direction: OrderByDirection,
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity,
|
||||
isNestedDateField?: boolean,
|
||||
): ObjectRecordOrderByForScalarField | ObjectRecordOrderByForRelationField => {
|
||||
if (!isDefined(groupBySubFieldName)) {
|
||||
return {
|
||||
@@ -19,6 +23,17 @@ export const getRelationFieldOrderBy = (
|
||||
|
||||
const [nestedFieldName, nestedSubFieldName] = groupBySubFieldName.split('.');
|
||||
|
||||
if (isNestedDateField === true || isDefined(dateGranularity)) {
|
||||
return {
|
||||
[groupByField.name]: {
|
||||
[nestedFieldName]: {
|
||||
orderBy: direction,
|
||||
granularity: dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDefined(nestedSubFieldName)) {
|
||||
return {
|
||||
[groupByField.name]: {
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
|
||||
export const isNestedFieldDateType = (
|
||||
field: FieldMetadataItem,
|
||||
subFieldName: string | undefined,
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
): boolean => {
|
||||
if (!isDefined(subFieldName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isFieldRelation(field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetObjectNameSingular =
|
||||
field.relation?.targetObjectMetadata?.nameSingular;
|
||||
|
||||
if (!isDefined(targetObjectNameSingular)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === targetObjectNameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(targetObjectMetadataItem)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nestedFieldName = subFieldName.split('.')[0];
|
||||
const nestedField = targetObjectMetadataItem.fields.find(
|
||||
(f) => f.name === nestedFieldName,
|
||||
);
|
||||
|
||||
if (!isDefined(nestedField)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isFieldMetadataDateKind(nestedField.type);
|
||||
};
|
||||
Reference in New Issue
Block a user