[DASHBOARDS] Add split multi-value fields setting for charts (#17907)
closes https://github.com/twentyhq/twenty/issues/17890 https://github.com/user-attachments/assets/dbf2ebe6-c2da-44d0-846e-d7fa9fac5f43
This commit is contained in:
@@ -419,6 +419,7 @@ export type BarChartConfiguration = {
|
||||
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
secondaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
splitMultiValueFields?: Maybe<Scalars['Boolean']>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
@@ -1936,6 +1937,7 @@ export type LineChartConfiguration = {
|
||||
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
secondaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
splitMultiValueFields?: Maybe<Scalars['Boolean']>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
@@ -3540,6 +3542,7 @@ export type PieChartConfiguration = {
|
||||
manualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
orderBy?: Maybe<GraphOrderBy>;
|
||||
showCenterMetric?: Maybe<Scalars['Boolean']>;
|
||||
splitMultiValueFields?: Maybe<Scalars['Boolean']>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
+4
@@ -16,6 +16,8 @@ import { RANGE_MAX_SETTING } from '@/command-menu/pages/page-layout/constants/se
|
||||
import { RANGE_MIN_SETTING } from '@/command-menu/pages/page-layout/constants/settings/RangeMinSetting';
|
||||
import { SHOW_LEGEND_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ShowLegendSetting';
|
||||
import { SORT_BY_GROUP_BY_FIELD_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting';
|
||||
import { SPLIT_MULTI_VALUE_FIELDS_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SplitMultiValueFieldsXSetting';
|
||||
import { SPLIT_MULTI_VALUE_FIELDS_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SplitMultiValueFieldsYSetting';
|
||||
import { STACKED_LINES_SETTING } from '@/command-menu/pages/page-layout/constants/settings/StackedLineSettings';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
|
||||
@@ -31,6 +33,7 @@ export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
DATE_GRANULARITY_X_SETTING,
|
||||
PRIMARY_SORT_BY_SETTING,
|
||||
OMIT_NULL_VALUES_SETTING,
|
||||
SPLIT_MULTI_VALUE_FIELDS_X_SETTING,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -43,6 +46,7 @@ export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
CUMULATIVE_SETTING,
|
||||
RANGE_MIN_SETTING,
|
||||
RANGE_MAX_SETTING,
|
||||
SPLIT_MULTI_VALUE_FIELDS_Y_SETTING,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+2
@@ -10,6 +10,7 @@ import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/setti
|
||||
import { HIDE_EMPTY_CATEGORY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/HideEmptyCategorySetting';
|
||||
import { PRIMARY_SORT_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/PrimarySortBySetting';
|
||||
import { SHOW_LEGEND_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ShowLegendSetting';
|
||||
import { SPLIT_MULTI_VALUE_FIELDS_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SplitMultiValueFieldsXSetting';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
|
||||
export const PIE_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
@@ -23,6 +24,7 @@ export const PIE_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
EACH_SLICE_REPRESENTS_SETTING,
|
||||
PRIMARY_SORT_BY_SETTING,
|
||||
HIDE_EMPTY_CATEGORY_SETTING,
|
||||
SPLIT_MULTI_VALUE_FIELDS_X_SETTING,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+2
@@ -28,4 +28,6 @@ export const CHART_CONFIGURATION_SETTING_LABELS = {
|
||||
PREFIX: msg`Prefix`,
|
||||
SUFFIX: msg`Suffix`,
|
||||
CUMULATIVE: msg`Cumulative`,
|
||||
SPLIT_MULTI_VALUE_FIELDS_X: msg`Split multiple values`,
|
||||
SPLIT_MULTI_VALUE_FIELDS_Y: msg`Split multiple values`,
|
||||
};
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconArrowsSplit2 } from 'twenty-ui/display';
|
||||
|
||||
export const SPLIT_MULTI_VALUE_FIELDS_X_SETTING: ChartSettingsItem = {
|
||||
isBoolean: true,
|
||||
Icon: IconArrowsSplit2,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.SPLIT_MULTI_VALUE_FIELDS_X,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_X,
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels';
|
||||
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
|
||||
import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconArrowsSplit2 } from 'twenty-ui/display';
|
||||
|
||||
export const SPLIT_MULTI_VALUE_FIELDS_Y_SETTING: ChartSettingsItem = {
|
||||
isBoolean: true,
|
||||
Icon: IconArrowsSplit2,
|
||||
label: CHART_CONFIGURATION_SETTING_LABELS.SPLIT_MULTI_VALUE_FIELDS_Y,
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_Y,
|
||||
};
|
||||
+5
@@ -257,6 +257,11 @@ export const useChartSettingsValues = ({
|
||||
return isBarOrLineChart
|
||||
? (configuration.omitNullValues ?? false)
|
||||
: false;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_X:
|
||||
case CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_Y:
|
||||
return isBarOrLineChart || isPieChart
|
||||
? (configuration.splitMultiValueFields ?? true)
|
||||
: true;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.HIDE_EMPTY_CATEGORY:
|
||||
return isPieChart ? (configuration.hideEmptyCategory ?? false) : false;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE:
|
||||
|
||||
+1
@@ -8,4 +8,5 @@ export type BarLineChartConvertibleFields = {
|
||||
primaryAxisGroupBySubFieldName?: string | null;
|
||||
primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null;
|
||||
primaryAxisOrderBy?: GraphOrderBy | null;
|
||||
splitMultiValueFields?: boolean | null;
|
||||
};
|
||||
|
||||
+6
@@ -27,6 +27,8 @@ export enum CHART_CONFIGURATION_SETTING_IDS {
|
||||
PREFIX = 'PREFIX',
|
||||
SUFFIX = 'SUFFIX',
|
||||
CUMULATIVE = 'CUMULATIVE',
|
||||
SPLIT_MULTI_VALUE_FIELDS_X = 'SPLIT_MULTI_VALUE_FIELDS_X',
|
||||
SPLIT_MULTI_VALUE_FIELDS_Y = 'SPLIT_MULTI_VALUE_FIELDS_Y',
|
||||
}
|
||||
|
||||
export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = {
|
||||
@@ -47,4 +49,8 @@ export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = {
|
||||
[CHART_CONFIGURATION_SETTING_IDS.PREFIX]: 'prefix',
|
||||
[CHART_CONFIGURATION_SETTING_IDS.SUFFIX]: 'suffix',
|
||||
[CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE]: 'isCumulative',
|
||||
[CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_X]:
|
||||
'splitMultiValueFields',
|
||||
[CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_Y]:
|
||||
'splitMultiValueFields',
|
||||
} as const;
|
||||
|
||||
+1
@@ -8,4 +8,5 @@ export type PieChartConvertibleFields = {
|
||||
groupBySubFieldName?: string | null;
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity | null;
|
||||
orderBy?: GraphOrderBy | null;
|
||||
splitMultiValueFields?: boolean | null;
|
||||
};
|
||||
|
||||
+81
-1
@@ -1,7 +1,12 @@
|
||||
import { type TypedBarChartConfiguration } from '@/command-menu/pages/page-layout/types/TypedBarChartConfiguration';
|
||||
import { type TypedLineChartConfiguration } from '@/command-menu/pages/page-layout/types/TypedLineChartConfiguration';
|
||||
import { type TypedPieChartConfiguration } from '@/command-menu/pages/page-layout/types/TypedPieChartConfiguration';
|
||||
import { buildChartGroupByFieldConfigUpdate } from '@/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate';
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
BarChartGroupMode,
|
||||
GraphOrderBy,
|
||||
@@ -9,6 +14,41 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
describe('buildChartGroupByFieldConfigUpdate', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'object-id',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
labelSingular: 'Opportunity',
|
||||
labelPlural: 'Opportunities',
|
||||
description: 'An opportunity',
|
||||
icon: 'IconTargetArrow',
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isRemote: false,
|
||||
isAuditLogged: false,
|
||||
isLabelSyncedWithName: true,
|
||||
fields: [
|
||||
{
|
||||
id: 'text-field-id',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
id: 'array-field-id',
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
},
|
||||
{
|
||||
id: 'array-field-id-2',
|
||||
name: 'skills',
|
||||
label: 'Skills',
|
||||
type: FieldMetadataType.ARRAY,
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
it('sets default orderBy and dateGranularity for primary axis', () => {
|
||||
const result = buildChartGroupByFieldConfigUpdate({
|
||||
configuration: {
|
||||
@@ -97,4 +137,44 @@ describe('buildChartGroupByFieldConfigUpdate', () => {
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables split multi-value fields for bar charts when both axes are arrays', () => {
|
||||
const result = buildChartGroupByFieldConfigUpdate({
|
||||
configuration: {
|
||||
__typename: 'BarChartConfiguration',
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
splitMultiValueFields: true,
|
||||
primaryAxisGroupByFieldMetadataId: 'array-field-id',
|
||||
} as TypedBarChartConfiguration,
|
||||
fieldMetadataIdKey: 'secondaryAxisGroupByFieldMetadataId',
|
||||
subFieldNameKey: 'secondaryAxisGroupBySubFieldName',
|
||||
fieldId: 'array-field-id-2',
|
||||
subFieldName: null,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
splitMultiValueFields: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables split multi-value fields for line charts when both axes are arrays', () => {
|
||||
const result = buildChartGroupByFieldConfigUpdate({
|
||||
configuration: {
|
||||
__typename: 'LineChartConfiguration',
|
||||
configurationType: WidgetConfigurationType.LINE_CHART,
|
||||
splitMultiValueFields: true,
|
||||
secondaryAxisGroupByFieldMetadataId: 'array-field-id-2',
|
||||
} as TypedLineChartConfiguration,
|
||||
fieldMetadataIdKey: 'primaryAxisGroupByFieldMetadataId',
|
||||
subFieldNameKey: 'primaryAxisGroupBySubFieldName',
|
||||
fieldId: 'array-field-id',
|
||||
subFieldName: null,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
splitMultiValueFields: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
-4
@@ -31,7 +31,7 @@ describe('getBarChartSettings', () => {
|
||||
);
|
||||
|
||||
expect(xAxisGroup).toBeDefined();
|
||||
expect(xAxisGroup?.items).toHaveLength(4);
|
||||
expect(xAxisGroup?.items).toHaveLength(5);
|
||||
expect(xAxisGroup?.items[0].id).toBe(DATA_DISPLAY_X_SETTING.id);
|
||||
expect(xAxisGroup?.items[0].label).toBe(DATA_DISPLAY_X_SETTING.label);
|
||||
expect(xAxisGroup?.items[0].Icon).toBe(IconAxisX);
|
||||
@@ -48,7 +48,7 @@ describe('getBarChartSettings', () => {
|
||||
);
|
||||
|
||||
expect(yAxisGroup).toBeDefined();
|
||||
expect(yAxisGroup?.items).toHaveLength(7);
|
||||
expect(yAxisGroup?.items).toHaveLength(8);
|
||||
expect(yAxisGroup?.items[0].id).toBe(DATA_DISPLAY_Y_SETTING.id);
|
||||
expect(yAxisGroup?.items[0].label).toBe(DATA_DISPLAY_Y_SETTING.label);
|
||||
expect(yAxisGroup?.items[0].Icon).toBe(IconAxisY);
|
||||
@@ -80,7 +80,7 @@ describe('getBarChartSettings', () => {
|
||||
);
|
||||
|
||||
expect(xAxisGroup).toBeDefined();
|
||||
expect(xAxisGroup?.items).toHaveLength(7);
|
||||
expect(xAxisGroup?.items).toHaveLength(8);
|
||||
expect(xAxisGroup?.items[0].id).toBe(DATA_DISPLAY_Y_SETTING.id);
|
||||
expect(xAxisGroup?.items[0].label).toBe(DATA_DISPLAY_Y_SETTING.label);
|
||||
expect(xAxisGroup?.items[0].Icon).toBe(IconAxisX);
|
||||
@@ -100,7 +100,7 @@ describe('getBarChartSettings', () => {
|
||||
);
|
||||
|
||||
expect(yAxisGroup).toBeDefined();
|
||||
expect(yAxisGroup?.items).toHaveLength(4);
|
||||
expect(yAxisGroup?.items).toHaveLength(5);
|
||||
expect(yAxisGroup?.items[0].id).toBe(DATA_DISPLAY_X_SETTING.id);
|
||||
expect(yAxisGroup?.items[0].label).toBe(DATA_DISPLAY_X_SETTING.label);
|
||||
expect(yAxisGroup?.items[0].Icon).toBe(IconAxisY);
|
||||
|
||||
+151
@@ -485,4 +485,155 @@ describe('shouldHideChartSetting', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('split multi-value fields visibility based on grouped field types', () => {
|
||||
const splitMultiValueFieldsXItem: ChartSettingsItem = {
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_X,
|
||||
label: msg`Split multiple values`,
|
||||
Icon: IconChartBar,
|
||||
isBoolean: true,
|
||||
isNumberInput: false,
|
||||
};
|
||||
|
||||
const splitMultiValueFieldsYItem: ChartSettingsItem = {
|
||||
id: CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_Y,
|
||||
label: msg`Split multiple values`,
|
||||
Icon: IconChartBar,
|
||||
isBoolean: true,
|
||||
isNumberInput: false,
|
||||
};
|
||||
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'object-id',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
labelSingular: 'Opportunity',
|
||||
labelPlural: 'Opportunities',
|
||||
description: 'An opportunity',
|
||||
icon: 'IconTargetArrow',
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isRemote: false,
|
||||
isAuditLogged: false,
|
||||
isLabelSyncedWithName: true,
|
||||
fields: [
|
||||
{
|
||||
id: 'text-field-id',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
id: 'array-field-id',
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
},
|
||||
{
|
||||
id: 'array-field-id-2',
|
||||
name: 'skills',
|
||||
label: 'Skills',
|
||||
type: FieldMetadataType.ARRAY,
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
it('should show X setting when bar chart primary group by field is an array field', () => {
|
||||
const barChartConfig: ChartConfiguration = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'array-field-id',
|
||||
} as any;
|
||||
|
||||
const result = shouldHideChartSetting(
|
||||
splitMultiValueFieldsXItem,
|
||||
'object-id',
|
||||
true,
|
||||
barChartConfig,
|
||||
mockObjectMetadataItem,
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should hide X setting when primary group by field is not an array field', () => {
|
||||
const barChartConfig: ChartConfiguration = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'text-field-id',
|
||||
secondaryAxisGroupByFieldMetadataId: 'array-field-id',
|
||||
} as any;
|
||||
|
||||
const result = shouldHideChartSetting(
|
||||
splitMultiValueFieldsXItem,
|
||||
'object-id',
|
||||
true,
|
||||
barChartConfig,
|
||||
mockObjectMetadataItem,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should show Y setting when secondary group by field is an array field', () => {
|
||||
const barChartConfig: ChartConfiguration = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'text-field-id',
|
||||
secondaryAxisGroupByFieldMetadataId: 'array-field-id',
|
||||
} as any;
|
||||
|
||||
const result = shouldHideChartSetting(
|
||||
splitMultiValueFieldsYItem,
|
||||
'object-id',
|
||||
true,
|
||||
barChartConfig,
|
||||
mockObjectMetadataItem,
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should hide Y setting when secondary group by field is not an array field', () => {
|
||||
const barChartConfig: ChartConfiguration = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'array-field-id',
|
||||
secondaryAxisGroupByFieldMetadataId: 'text-field-id',
|
||||
} as any;
|
||||
|
||||
const result = shouldHideChartSetting(
|
||||
splitMultiValueFieldsYItem,
|
||||
'object-id',
|
||||
true,
|
||||
barChartConfig,
|
||||
mockObjectMetadataItem,
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide both X and Y settings when both axes are array fields', () => {
|
||||
const barChartConfig: ChartConfiguration = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'array-field-id',
|
||||
secondaryAxisGroupByFieldMetadataId: 'array-field-id-2',
|
||||
} as any;
|
||||
|
||||
const resultX = shouldHideChartSetting(
|
||||
splitMultiValueFieldsXItem,
|
||||
'object-id',
|
||||
true,
|
||||
barChartConfig,
|
||||
mockObjectMetadataItem,
|
||||
);
|
||||
|
||||
const resultY = shouldHideChartSetting(
|
||||
splitMultiValueFieldsYItem,
|
||||
'object-id',
|
||||
true,
|
||||
barChartConfig,
|
||||
mockObjectMetadataItem,
|
||||
);
|
||||
|
||||
expect(resultX).toBe(true);
|
||||
expect(resultY).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+40
-1
@@ -4,7 +4,7 @@ import { isFieldOrRelationNestedFieldDateKind } from '@/command-menu/pages/page-
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, isFieldMetadataArrayKind } from 'twenty-shared/utils';
|
||||
import { BarChartGroupMode, GraphOrderBy } from '~/generated-metadata/graphql';
|
||||
|
||||
type BuildChartGroupByFieldConfigUpdateArgs<T extends ChartConfiguration> = {
|
||||
@@ -60,6 +60,23 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
? getChartDefaultOrderByForFieldType(fieldMetadataItem?.type)
|
||||
: GraphOrderBy.FIELD_ASC;
|
||||
|
||||
const isArrayFieldMetadataId = (
|
||||
fieldMetadataId: string | null | undefined,
|
||||
): boolean => {
|
||||
if (!isDefined(fieldMetadataId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidateFieldMetadataItem = objectMetadataItem?.fields?.find(
|
||||
(field) => field.id === fieldMetadataId,
|
||||
);
|
||||
|
||||
return (
|
||||
isDefined(candidateFieldMetadataItem) &&
|
||||
isFieldMetadataArrayKind(candidateFieldMetadataItem.type)
|
||||
);
|
||||
};
|
||||
|
||||
if (isPrimaryAxis) {
|
||||
const existingDateGranularity =
|
||||
isBarChart || isLineChart
|
||||
@@ -79,6 +96,11 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
!isNewFieldDateType &&
|
||||
(isBarChart || isLineChart);
|
||||
|
||||
const shouldDisableSplitMultiValueFields =
|
||||
(isBarChart || isLineChart) &&
|
||||
isArrayFieldMetadataId(fieldId) &&
|
||||
isArrayFieldMetadataId(configuration.secondaryAxisGroupByFieldMetadataId);
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
primaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
|
||||
@@ -86,6 +108,9 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY)
|
||||
: null,
|
||||
...(shouldResetCumulative ? { isCumulative: false } : {}),
|
||||
...(shouldDisableSplitMultiValueFields
|
||||
? { splitMultiValueFields: false }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,6 +133,10 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
}
|
||||
|
||||
if (isBarChart) {
|
||||
const shouldDisableSplitMultiValueFields =
|
||||
isArrayFieldMetadataId(configuration.primaryAxisGroupByFieldMetadataId) &&
|
||||
isArrayFieldMetadataId(fieldId);
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
secondaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
|
||||
@@ -118,10 +147,17 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
groupMode: isDefined(fieldId)
|
||||
? (configuration.groupMode ?? BarChartGroupMode.STACKED)
|
||||
: null,
|
||||
...(shouldDisableSplitMultiValueFields
|
||||
? { splitMultiValueFields: false }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
if (isLineChart) {
|
||||
const shouldDisableSplitMultiValueFields =
|
||||
isArrayFieldMetadataId(configuration.primaryAxisGroupByFieldMetadataId) &&
|
||||
isArrayFieldMetadataId(fieldId);
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
secondaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
|
||||
@@ -130,6 +166,9 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
ObjectRecordGroupByDateGranularity.DAY)
|
||||
: null,
|
||||
isStacked: isDefined(fieldId) ? (configuration.isStacked ?? true) : null,
|
||||
...(shouldDisableSplitMultiValueFields
|
||||
? { splitMultiValueFields: false }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -19,5 +19,6 @@ export const convertBarOrLineChartConfigToPieChart = (
|
||||
groupBySubFieldName: configuration.primaryAxisGroupBySubFieldName,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity,
|
||||
orderBy: configuration.primaryAxisOrderBy,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -14,5 +14,6 @@ export const convertPieChartConfigToBarOrLineChart = (
|
||||
primaryAxisGroupBySubFieldName: configuration.groupBySubFieldName,
|
||||
primaryAxisDateGranularity: configuration.dateGranularity,
|
||||
primaryAxisOrderBy: configuration.orderBy,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
};
|
||||
};
|
||||
|
||||
+4
@@ -16,6 +16,8 @@ import { RANGE_MAX_SETTING } from '@/command-menu/pages/page-layout/constants/se
|
||||
import { RANGE_MIN_SETTING } from '@/command-menu/pages/page-layout/constants/settings/RangeMinSetting';
|
||||
import { SHOW_LEGEND_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ShowLegendSetting';
|
||||
import { SORT_BY_GROUP_BY_FIELD_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting';
|
||||
import { SPLIT_MULTI_VALUE_FIELDS_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SplitMultiValueFieldsXSetting';
|
||||
import { SPLIT_MULTI_VALUE_FIELDS_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SplitMultiValueFieldsYSetting';
|
||||
import { STACKED_BARS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/StackedBarsSetting';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconAxisX, IconAxisY } from 'twenty-ui/display';
|
||||
@@ -34,6 +36,7 @@ export const getBarChartSettings = (
|
||||
DATE_GRANULARITY_X_SETTING,
|
||||
PRIMARY_SORT_BY_SETTING,
|
||||
OMIT_NULL_VALUES_SETTING,
|
||||
SPLIT_MULTI_VALUE_FIELDS_X_SETTING,
|
||||
];
|
||||
|
||||
const secondaryAxisItems = [
|
||||
@@ -44,6 +47,7 @@ export const getBarChartSettings = (
|
||||
CUMULATIVE_SETTING,
|
||||
RANGE_MIN_SETTING,
|
||||
RANGE_MAX_SETTING,
|
||||
SPLIT_MULTI_VALUE_FIELDS_Y_SETTING,
|
||||
];
|
||||
|
||||
const xAxisItems = isHorizontal ? secondaryAxisItems : primaryAxisItems;
|
||||
|
||||
+59
-1
@@ -6,7 +6,11 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import {
|
||||
isDefined,
|
||||
isFieldMetadataArrayKind,
|
||||
isFieldMetadataDateKind,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
const shouldHideDateGranularityBasedOnFieldType = (
|
||||
fieldMetadataId: string | undefined | null,
|
||||
@@ -102,6 +106,60 @@ export const shouldHideChartSetting = (
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
item.id === CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_X ||
|
||||
item.id === CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_Y
|
||||
) {
|
||||
const isXAxis =
|
||||
item.id === CHART_CONFIGURATION_SETTING_IDS.SPLIT_MULTI_VALUE_FIELDS_X;
|
||||
|
||||
let fieldMetadataId: string | null | undefined;
|
||||
|
||||
if (isBarOrLineChart) {
|
||||
fieldMetadataId = isXAxis
|
||||
? configuration.primaryAxisGroupByFieldMetadataId
|
||||
: configuration.secondaryAxisGroupByFieldMetadataId;
|
||||
} else if (
|
||||
isWidgetConfigurationOfType(configuration, 'PieChartConfiguration')
|
||||
) {
|
||||
fieldMetadataId = isXAxis
|
||||
? configuration.groupByFieldMetadataId
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (!isDefined(fieldMetadataId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isBarOrLineChart) {
|
||||
const primaryField = objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.id === configuration.primaryAxisGroupByFieldMetadataId,
|
||||
);
|
||||
const secondaryField = objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.id === configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
);
|
||||
const bothAxesAreArrayFields =
|
||||
isDefined(primaryField) &&
|
||||
isFieldMetadataArrayKind(primaryField.type) &&
|
||||
isDefined(secondaryField) &&
|
||||
isFieldMetadataArrayKind(secondaryField.type);
|
||||
|
||||
if (bothAxesAreArrayFields === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const groupByField = objectMetadataItem.fields.find(
|
||||
(field) => field.id === fieldMetadataId,
|
||||
);
|
||||
|
||||
return (
|
||||
!isDefined(groupByField) || !isFieldMetadataArrayKind(groupByField.type)
|
||||
);
|
||||
}
|
||||
|
||||
if (item.id === CHART_CONFIGURATION_SETTING_IDS.SHOW_LEGEND) {
|
||||
if (isWidgetConfigurationOfType(configuration, 'PieChartConfiguration')) {
|
||||
return false;
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
|
||||
export type GroupByRegularField = {
|
||||
fieldMetadata: FlatFieldMetadata;
|
||||
subFieldName?: string;
|
||||
shouldUnnest?: boolean;
|
||||
};
|
||||
|
||||
export type GroupByDateField = {
|
||||
|
||||
+46
-1
@@ -3,7 +3,10 @@ import {
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type GroupByDateField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import {
|
||||
type GroupByDateField,
|
||||
type GroupByRegularField,
|
||||
} from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import { getGroupByExpression } from 'src/engine/api/common/common-query-runners/utils/get-group-by-expression.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
@@ -23,6 +26,16 @@ const buildGroupByDateField = (
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const buildGroupByRegularField = (
|
||||
overrides: Partial<GroupByRegularField> = {},
|
||||
): GroupByRegularField => ({
|
||||
fieldMetadata: {
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
name: 'tags',
|
||||
} as FlatFieldMetadata,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('getGroupByExpression', () => {
|
||||
const columnNameWithQuotes = '"company"."createdAt"';
|
||||
|
||||
@@ -126,4 +139,36 @@ describe('getGroupByExpression', () => {
|
||||
expect(result).not.toContain('AT TIME ZONE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('array unnest handling', () => {
|
||||
const arrayColumnNameWithQuotes = '"company"."tags"';
|
||||
|
||||
it('should unnest array fields when split mode is enabled', () => {
|
||||
const groupByField = buildGroupByRegularField({
|
||||
shouldUnnest: true,
|
||||
});
|
||||
|
||||
const result = getGroupByExpression({
|
||||
groupByField,
|
||||
columnNameWithQuotes: arrayColumnNameWithQuotes,
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`UNNEST(CASE WHEN CARDINALITY(${arrayColumnNameWithQuotes}) > 0 THEN ${arrayColumnNameWithQuotes} ELSE ARRAY[${arrayColumnNameWithQuotes}[1]] END)`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep plain column expression when split mode is disabled', () => {
|
||||
const groupByField = buildGroupByRegularField({
|
||||
shouldUnnest: false,
|
||||
});
|
||||
|
||||
const result = getGroupByExpression({
|
||||
groupByField,
|
||||
columnNameWithQuotes: arrayColumnNameWithQuotes,
|
||||
});
|
||||
|
||||
expect(result).toBe(arrayColumnNameWithQuotes);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
@@ -30,6 +30,10 @@ export const getGroupByExpression = ({
|
||||
if (
|
||||
!(isGroupByDateField(groupByField) || isGroupByRelationField(groupByField))
|
||||
) {
|
||||
if ('shouldUnnest' in groupByField && groupByField.shouldUnnest) {
|
||||
return `UNNEST(CASE WHEN CARDINALITY(${columnNameWithQuotes}) > 0 THEN ${columnNameWithQuotes} ELSE ARRAY[${columnNameWithQuotes}[1]] END)`;
|
||||
}
|
||||
|
||||
return columnNameWithQuotes;
|
||||
}
|
||||
|
||||
|
||||
+15
-8
@@ -12,16 +12,16 @@ import {
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { convertOrderByToFindOptionsOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order';
|
||||
import { getOptionalOrderByCasting } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/get-optional-order-by-casting.util';
|
||||
import { parseCompositeFieldForOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util';
|
||||
import { prepareForOrderByRelationFieldParsing } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util';
|
||||
import {
|
||||
type GroupByDateField,
|
||||
type GroupByField,
|
||||
type GroupByRegularField,
|
||||
} from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import { getGroupByOrderExpression } from 'src/engine/api/common/common-query-runners/utils/get-group-by-order-expression.util';
|
||||
import { convertOrderByToFindOptionsOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order';
|
||||
import { getOptionalOrderByCasting } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/get-optional-order-by-casting.util';
|
||||
import { parseCompositeFieldForOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util';
|
||||
import { prepareForOrderByRelationFieldParsing } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import {
|
||||
type AggregationField,
|
||||
@@ -31,12 +31,12 @@ import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-er
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
|
||||
import { type OrderByClause } from './types/order-by-condition.type';
|
||||
|
||||
@@ -387,11 +387,11 @@ export class GraphqlQueryOrderGroupByParser {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
fieldMetadata: FlatFieldMetadata;
|
||||
}): Record<string, OrderByClause> | null => {
|
||||
const fieldIsInGroupBy = groupByFields.some(
|
||||
const groupByField = groupByFields.find(
|
||||
(groupByField) => groupByField.fieldMetadata.id === fieldMetadata.id,
|
||||
);
|
||||
|
||||
if (!fieldIsInGroupBy) {
|
||||
if (!isDefined(groupByField)) {
|
||||
throw new UserInputError(
|
||||
`Cannot order by a field that is not an aggregate nor in groupBy criteria: ${fieldMetadata.name}.`,
|
||||
);
|
||||
@@ -404,8 +404,15 @@ export class GraphqlQueryOrderGroupByParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columnNameWithQuotes = `"${flatObjectMetadata.nameSingular}"."${fieldMetadata.name}"`;
|
||||
|
||||
const expression = getGroupByOrderExpression({
|
||||
groupByField,
|
||||
columnNameWithQuotes,
|
||||
});
|
||||
|
||||
return {
|
||||
[`"${flatObjectMetadata.nameSingular}"."${fieldMetadata.name}"${orderByCasting}`]:
|
||||
[`${expression}${orderByCasting}`]:
|
||||
convertOrderByToFindOptionsOrder(orderByDirection),
|
||||
};
|
||||
};
|
||||
|
||||
+14
@@ -93,6 +93,20 @@ export const parseGroupByArgs = (
|
||||
}
|
||||
}
|
||||
|
||||
// Handle array unnest fields
|
||||
if (
|
||||
typeof fieldNames[fieldName] === 'object' &&
|
||||
fieldNames[fieldName] !== null &&
|
||||
'unnest' in fieldNames[fieldName]
|
||||
) {
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
subFieldName: undefined,
|
||||
shouldUnnest: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle regular fields and composite fields
|
||||
if (fieldNames[fieldName] === true) {
|
||||
groupByFields.push({
|
||||
|
||||
+5
@@ -112,6 +112,11 @@ export class BarChartConfigurationDTO
|
||||
@IsOptional()
|
||||
omitNullValues?: boolean;
|
||||
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
splitMultiValueFields?: boolean;
|
||||
|
||||
@Field(() => AxisNameDisplay, {
|
||||
nullable: true,
|
||||
defaultValue: AxisNameDisplay.NONE,
|
||||
|
||||
+5
@@ -113,6 +113,11 @@ export class LineChartConfigurationDTO
|
||||
@IsOptional()
|
||||
omitNullValues?: boolean;
|
||||
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
splitMultiValueFields?: boolean;
|
||||
|
||||
@Field(() => AxisNameDisplay, {
|
||||
nullable: true,
|
||||
defaultValue: AxisNameDisplay.NONE,
|
||||
|
||||
+5
@@ -97,6 +97,11 @@ export class PieChartConfigurationDTO
|
||||
@IsOptional()
|
||||
hideEmptyCategory?: boolean;
|
||||
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
splitMultiValueFields?: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
+4
-1
@@ -9,8 +9,8 @@ import {
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto';
|
||||
import { BarChartGroupMode } from 'src/engine/metadata-modules/page-layout-widget/enums/bar-chart-group-mode.enum';
|
||||
@@ -166,6 +166,7 @@ export class BarChartDataService {
|
||||
secondaryDateGranularity:
|
||||
configuration.secondaryAxisGroupByDateGranularity,
|
||||
secondaryAxisOrderBy: configuration.secondaryAxisOrderBy,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
});
|
||||
|
||||
if (isTwoDimensional && isDefined(secondaryAxisGroupByField)) {
|
||||
@@ -249,6 +250,7 @@ export class BarChartDataService {
|
||||
omitNullValues: configuration.omitNullValues ?? false,
|
||||
isDescOrder,
|
||||
isTwoDimensional: false,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
});
|
||||
|
||||
const selectOptions = getSelectOptions(primaryAxisGroupByField);
|
||||
@@ -389,6 +391,7 @@ export class BarChartDataService {
|
||||
omitNullValues: configuration.omitNullValues ?? false,
|
||||
isDescOrder,
|
||||
isTwoDimensional: true,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
});
|
||||
|
||||
const primarySelectOptions = getSelectOptions(primaryAxisGroupByField);
|
||||
|
||||
+34
-1
@@ -5,7 +5,11 @@ import {
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
OrderByWithGroupBy,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import {
|
||||
isDefined,
|
||||
isFieldMetadataArrayKind,
|
||||
isFieldMetadataDateKind,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectRecordGroupBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
@@ -19,6 +23,11 @@ import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enu
|
||||
import { ChartFilter } from 'src/engine/metadata-modules/page-layout-widget/types/chart-filter.type';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from 'src/modules/dashboard/chart-data/constants/graph-default-date-granularity.constant';
|
||||
import { GRAPH_DEFAULT_ORDER_BY } from 'src/modules/dashboard/chart-data/constants/graph-default-order-by.constant';
|
||||
import {
|
||||
ChartDataException,
|
||||
ChartDataExceptionCode,
|
||||
generateChartDataExceptionMessage,
|
||||
} from 'src/modules/dashboard/chart-data/exceptions/chart-data.exception';
|
||||
import { GroupByRawResult } from 'src/modules/dashboard/chart-data/types/group-by-raw-result.type';
|
||||
import { buildAggregateFieldKey } from 'src/modules/dashboard/chart-data/utils/build-aggregate-field-key.util';
|
||||
import {
|
||||
@@ -51,6 +60,7 @@ type ExecuteGroupByQueryParams = {
|
||||
secondaryDateGranularity?: ObjectRecordGroupByDateGranularity;
|
||||
primaryAxisOrderBy?: GraphOrderBy;
|
||||
secondaryAxisOrderBy?: GraphOrderBy;
|
||||
splitMultiValueFields?: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -79,6 +89,7 @@ export class ChartDataQueryService {
|
||||
secondaryGroupBySubFieldName,
|
||||
secondaryDateGranularity,
|
||||
secondaryAxisOrderBy,
|
||||
splitMultiValueFields,
|
||||
}: ExecuteGroupByQueryParams): Promise<GroupByRawResult[]> {
|
||||
const gqlOperationFilter = convertChartFilterToGqlOperationFilter({
|
||||
filter,
|
||||
@@ -111,6 +122,12 @@ export class ChartDataQueryService {
|
||||
const shouldApplyPrimaryDateGranularity =
|
||||
isPrimaryFieldDate || isPrimaryNestedDate;
|
||||
|
||||
const shouldSplitMultiValueFields = splitMultiValueFields ?? true;
|
||||
|
||||
const shouldUnnestPrimary =
|
||||
shouldSplitMultiValueFields &&
|
||||
isFieldMetadataArrayKind(primaryGroupByField.type);
|
||||
|
||||
const groupBy: GroupByFieldObject[] = [];
|
||||
|
||||
groupBy.push(
|
||||
@@ -123,6 +140,7 @@ export class ChartDataQueryService {
|
||||
firstDayOfTheWeek,
|
||||
isNestedDateField: isPrimaryNestedDate,
|
||||
timeZone: userTimezone,
|
||||
shouldUnnest: shouldUnnestPrimary,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -162,6 +180,20 @@ export class ChartDataQueryService {
|
||||
const shouldApplySecondaryDateGranularity =
|
||||
isSecondaryFieldDate || isSecondaryNestedDate;
|
||||
|
||||
const shouldUnnestSecondary =
|
||||
shouldSplitMultiValueFields &&
|
||||
isFieldMetadataArrayKind(secondaryGroupByField.type);
|
||||
|
||||
if (shouldUnnestPrimary && shouldUnnestSecondary) {
|
||||
throw new ChartDataException(
|
||||
generateChartDataExceptionMessage(
|
||||
ChartDataExceptionCode.INVALID_WIDGET_CONFIGURATION,
|
||||
'Split multiple values can only be enabled when one grouped field is multi-value.',
|
||||
),
|
||||
ChartDataExceptionCode.INVALID_WIDGET_CONFIGURATION,
|
||||
);
|
||||
}
|
||||
|
||||
groupBy.push(
|
||||
buildGroupByFieldObject({
|
||||
fieldMetadata: secondaryGroupByField,
|
||||
@@ -172,6 +204,7 @@ export class ChartDataQueryService {
|
||||
firstDayOfTheWeek,
|
||||
isNestedDateField: isSecondaryNestedDate,
|
||||
timeZone: userTimezone,
|
||||
shouldUnnest: shouldUnnestSecondary,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+3
@@ -167,6 +167,7 @@ export class LineChartDataService {
|
||||
secondaryDateGranularity:
|
||||
configuration.secondaryAxisGroupByDateGranularity,
|
||||
secondaryAxisOrderBy: configuration.secondaryAxisOrderBy,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
});
|
||||
|
||||
const seriesIdPrefix = buildLineChartSeriesIdPrefix(
|
||||
@@ -256,6 +257,7 @@ export class LineChartDataService {
|
||||
omitNullValues: configuration.omitNullValues ?? false,
|
||||
isDescOrder,
|
||||
isTwoDimensional: false,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
});
|
||||
|
||||
const selectOptions = getSelectOptions(primaryAxisGroupByField);
|
||||
@@ -385,6 +387,7 @@ export class LineChartDataService {
|
||||
omitNullValues: configuration.omitNullValues ?? false,
|
||||
isDescOrder,
|
||||
isTwoDimensional: true,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
});
|
||||
|
||||
const primarySelectOptions = getSelectOptions(primaryAxisGroupByField);
|
||||
|
||||
+2
-1
@@ -8,8 +8,8 @@ import {
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
@@ -130,6 +130,7 @@ export class PieChartDataService {
|
||||
CalendarStartDay.MONDAY,
|
||||
limit,
|
||||
primaryAxisOrderBy: configuration.orderBy,
|
||||
splitMultiValueFields: configuration.splitMultiValueFields,
|
||||
});
|
||||
|
||||
return this.transformToPieChartData({
|
||||
|
||||
+8
-1
@@ -1,5 +1,6 @@
|
||||
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import {
|
||||
isFieldMetadataArrayKind,
|
||||
isFieldMetadataDateKind,
|
||||
isFieldMetadataSelectKind,
|
||||
} from 'twenty-shared/utils';
|
||||
@@ -23,6 +24,7 @@ type ApplyGapFillingParams = {
|
||||
omitNullValues: boolean;
|
||||
isDescOrder: boolean;
|
||||
isTwoDimensional: boolean;
|
||||
splitMultiValueFields?: boolean;
|
||||
};
|
||||
|
||||
type ApplyGapFillingResult = {
|
||||
@@ -37,6 +39,7 @@ export const applyGapFilling = ({
|
||||
omitNullValues,
|
||||
isDescOrder,
|
||||
isTwoDimensional,
|
||||
splitMultiValueFields,
|
||||
}: ApplyGapFillingParams): ApplyGapFillingResult => {
|
||||
if (omitNullValues) {
|
||||
return { data, wasTruncated: false };
|
||||
@@ -64,11 +67,15 @@ export const applyGapFilling = ({
|
||||
wasTruncated = dateResult.wasTruncated;
|
||||
}
|
||||
|
||||
const isArrayFieldWithoutSplit =
|
||||
isFieldMetadataArrayKind(primaryAxisGroupByField.type) &&
|
||||
!(splitMultiValueFields ?? true);
|
||||
|
||||
const isPrimaryFieldSelect = isFieldMetadataSelectKind(
|
||||
primaryAxisGroupByField.type,
|
||||
);
|
||||
|
||||
if (isPrimaryFieldSelect) {
|
||||
if (isPrimaryFieldSelect && !isArrayFieldWithoutSplit) {
|
||||
const selectOptions = getSelectOptions(primaryAxisGroupByField);
|
||||
|
||||
const fillSelectGapsFn = isTwoDimensional
|
||||
|
||||
+6
@@ -78,6 +78,7 @@ export type BuildGroupByFieldObjectParams = {
|
||||
firstDayOfTheWeek?: CalendarStartDay | null;
|
||||
isNestedDateField?: boolean;
|
||||
timeZone?: string;
|
||||
shouldUnnest?: boolean;
|
||||
};
|
||||
|
||||
export const buildGroupByFieldObject = ({
|
||||
@@ -87,6 +88,7 @@ export const buildGroupByFieldObject = ({
|
||||
firstDayOfTheWeek,
|
||||
isNestedDateField,
|
||||
timeZone,
|
||||
shouldUnnest,
|
||||
}: BuildGroupByFieldObjectParams): GroupByFieldObject => {
|
||||
const isRelation = isMorphOrRelationFlatFieldMetadata(fieldMetadata);
|
||||
const isComposite = isCompositeFieldMetadataType(fieldMetadata.type);
|
||||
@@ -156,5 +158,9 @@ export const buildGroupByFieldObject = ({
|
||||
return { [fieldMetadata.name]: dateGroupByObject };
|
||||
}
|
||||
|
||||
if (shouldUnnest) {
|
||||
return { [fieldMetadata.name]: { unnest: true } };
|
||||
}
|
||||
|
||||
return { [fieldMetadata.name]: true };
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './isFieldMetadataArrayKind';
|
||||
export * from './isFieldMetadataDateKind';
|
||||
export * from './isFieldMetadataNumericKind';
|
||||
export * from './isFieldMetadataSelectKind';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
|
||||
export const isFieldMetadataArrayKind = (
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
): boolean => {
|
||||
return (
|
||||
fieldMetadataType === FieldMetadataType.MULTI_SELECT ||
|
||||
fieldMetadataType === FieldMetadataType.ARRAY
|
||||
);
|
||||
};
|
||||
@@ -38,6 +38,7 @@ export { CustomError } from './errors/CustomError';
|
||||
export { evalFromContext } from './evalFromContext';
|
||||
export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';
|
||||
export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name';
|
||||
export { isFieldMetadataArrayKind } from './fieldMetadata/isFieldMetadataArrayKind';
|
||||
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
|
||||
export { isFieldMetadataNumericKind } from './fieldMetadata/isFieldMetadataNumericKind';
|
||||
export { isFieldMetadataSelectKind } from './fieldMetadata/isFieldMetadataSelectKind';
|
||||
|
||||
@@ -19,6 +19,7 @@ export {
|
||||
IconArrowRight,
|
||||
IconArrowsDiagonal,
|
||||
IconArrowsSort,
|
||||
IconArrowsSplit2,
|
||||
IconArrowsVertical,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
@@ -310,13 +311,13 @@ export {
|
||||
IconReload,
|
||||
IconReorder,
|
||||
IconRepeat,
|
||||
IconRowInsertBottom,
|
||||
IconRowInsertTop,
|
||||
IconRestore,
|
||||
IconRobot,
|
||||
IconRocket,
|
||||
IconRotate,
|
||||
IconRotate2,
|
||||
IconRowInsertBottom,
|
||||
IconRowInsertTop,
|
||||
IconSearch,
|
||||
IconSend,
|
||||
IconServer,
|
||||
|
||||
@@ -90,6 +90,7 @@ export {
|
||||
IconArrowRight,
|
||||
IconArrowsDiagonal,
|
||||
IconArrowsSort,
|
||||
IconArrowsSplit2,
|
||||
IconArrowsVertical,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
@@ -381,13 +382,13 @@ export {
|
||||
IconReload,
|
||||
IconReorder,
|
||||
IconRepeat,
|
||||
IconRowInsertBottom,
|
||||
IconRowInsertTop,
|
||||
IconRestore,
|
||||
IconRobot,
|
||||
IconRocket,
|
||||
IconRotate,
|
||||
IconRotate2,
|
||||
IconRowInsertBottom,
|
||||
IconRowInsertTop,
|
||||
IconSearch,
|
||||
IconSend,
|
||||
IconServer,
|
||||
|
||||
Reference in New Issue
Block a user