[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:
nitin
2026-02-17 13:59:17 +05:30
committed by GitHub
parent c3d565f266
commit cfc4e0e343
37 changed files with 557 additions and 24 deletions
@@ -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']>;
};
@@ -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,
],
},
{
@@ -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,
],
},
{
@@ -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`,
};
@@ -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,
};
@@ -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,
};
@@ -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:
@@ -8,4 +8,5 @@ export type BarLineChartConvertibleFields = {
primaryAxisGroupBySubFieldName?: string | null;
primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null;
primaryAxisOrderBy?: GraphOrderBy | null;
splitMultiValueFields?: boolean | null;
};
@@ -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;
@@ -8,4 +8,5 @@ export type PieChartConvertibleFields = {
groupBySubFieldName?: string | null;
dateGranularity?: ObjectRecordGroupByDateGranularity | null;
orderBy?: GraphOrderBy | null;
splitMultiValueFields?: boolean | null;
};
@@ -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,
});
});
});
@@ -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);
@@ -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);
});
});
});
@@ -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 }
: {}),
};
}
@@ -19,5 +19,6 @@ export const convertBarOrLineChartConfigToPieChart = (
groupBySubFieldName: configuration.primaryAxisGroupBySubFieldName,
dateGranularity: configuration.primaryAxisDateGranularity,
orderBy: configuration.primaryAxisOrderBy,
splitMultiValueFields: configuration.splitMultiValueFields,
};
};
@@ -14,5 +14,6 @@ export const convertPieChartConfigToBarOrLineChart = (
primaryAxisGroupBySubFieldName: configuration.groupBySubFieldName,
primaryAxisDateGranularity: configuration.dateGranularity,
primaryAxisOrderBy: configuration.orderBy,
splitMultiValueFields: configuration.splitMultiValueFields,
};
};
@@ -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;
@@ -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;
@@ -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 = {
@@ -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);
});
});
});
@@ -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;
}
@@ -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),
};
};
@@ -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({
@@ -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,
@@ -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,
@@ -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()
@@ -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);
@@ -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,
}),
);
@@ -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);
@@ -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({
@@ -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
@@ -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,
+3 -2
View File
@@ -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,