[DASHBOARDS] Add cumulative setting for bar chart and line chart (#16248)

## Description

- Add cumulative setting
- This setting is only present for dates
- It is preserved when switching from a date field to another date field
but it is reset when switching to another field type

## Video QA


https://github.com/user-attachments/assets/a070bf54-8ef6-4e35-9378-a514fe1c3848


https://github.com/user-attachments/assets/07edfe87-253c-45a5-b582-06f2df9a2dfc
This commit is contained in:
Raphaël Bosi
2025-12-02 14:00:58 +01:00
committed by GitHub
parent 00e970bdf4
commit 1038efa3dd
34 changed files with 766 additions and 19 deletions
@@ -123,6 +123,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
subFieldNameKey,
fieldId: fieldMetadataItem.id,
subFieldName: null,
objectMetadataItem: sourceObjectMetadataItem,
objectMetadataItems,
}),
});
closeDropdown();
@@ -136,6 +138,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
subFieldNameKey,
fieldId: null,
subFieldName: null,
objectMetadataItem: sourceObjectMetadataItem,
objectMetadataItems,
}),
});
closeDropdown();
@@ -161,6 +165,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
subFieldNameKey,
fieldId: selectedCompositeField.id,
subFieldName,
objectMetadataItem: sourceObjectMetadataItem,
objectMetadataItems,
}),
});
closeDropdown();
@@ -178,6 +184,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
subFieldNameKey,
fieldId: selectedRelationField.id,
subFieldName,
objectMetadataItem: sourceObjectMetadataItem,
objectMetadataItems,
}),
});
closeDropdown();
@@ -2,6 +2,7 @@ import { CHART_SETTINGS_HEADINGS } from '@/command-menu/pages/page-layout/consta
import { AXIS_NAME_SETTING } from '@/command-menu/pages/page-layout/constants/settings/AxisNameSetting';
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting';
import { CUMULATIVE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/CumulativeSetting';
import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting';
import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting';
import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting';
@@ -39,6 +40,7 @@ export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [
GROUP_BY_SETTING,
DATE_GRANULARITY_Y_SETTING,
SORT_BY_GROUP_BY_FIELD_SETTING,
CUMULATIVE_SETTING,
RANGE_MIN_SETTING,
RANGE_MAX_SETTING,
],
@@ -26,4 +26,5 @@ export const CHART_CONFIGURATION_SETTING_LABELS = {
SHOW_LEGEND: msg`Legend`,
PREFIX: msg`Prefix`,
SUFFIX: msg`Suffix`,
CUMULATIVE: msg`Cumulative`,
};
@@ -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 { IconChartBarPopular } from 'twenty-ui/display';
export const CUMULATIVE_SETTING: ChartSettingsItem = {
isBoolean: true,
Icon: IconChartBarPopular,
label: CHART_CONFIGURATION_SETTING_LABELS.CUMULATIVE,
id: CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE,
};
@@ -224,6 +224,8 @@ export const useChartSettingsValues = ({
return configuration.__typename === 'LineChartConfiguration'
? configuration.isStacked !== false
: true;
case CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE:
return isBarOrLineChart ? (configuration.isCumulative ?? false) : false;
case CHART_CONFIGURATION_SETTING_IDS.OMIT_NULL_VALUES:
return isBarOrLineChart
? (configuration.omitNullValues ?? false)
@@ -25,6 +25,7 @@ export enum CHART_CONFIGURATION_SETTING_IDS {
SHOW_LEGEND = 'SHOW_LEGEND',
PREFIX = 'PREFIX',
SUFFIX = 'SUFFIX',
CUMULATIVE = 'CUMULATIVE',
}
export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = {
@@ -43,4 +44,5 @@ export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = {
[CHART_CONFIGURATION_SETTING_IDS.SHOW_LEGEND]: 'displayLegend',
[CHART_CONFIGURATION_SETTING_IDS.PREFIX]: 'prefix',
[CHART_CONFIGURATION_SETTING_IDS.SUFFIX]: 'suffix',
[CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE]: 'isCumulative',
} as const;
@@ -2,6 +2,7 @@ import { CHART_SETTINGS_HEADINGS } from '@/command-menu/pages/page-layout/consta
import { AXIS_NAME_SETTING } from '@/command-menu/pages/page-layout/constants/settings/AxisNameSetting';
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting';
import { CUMULATIVE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/CumulativeSetting';
import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting';
import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting';
import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting';
@@ -47,15 +48,16 @@ describe('getBarChartSettings', () => {
);
expect(yAxisGroup).toBeDefined();
expect(yAxisGroup?.items).toHaveLength(6);
expect(yAxisGroup?.items).toHaveLength(7);
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);
expect(yAxisGroup?.items[1]).toEqual(GROUP_BY_SETTING);
expect(yAxisGroup?.items[2]).toEqual(DATE_GRANULARITY_Y_SETTING);
expect(yAxisGroup?.items[3]).toEqual(SORT_BY_GROUP_BY_FIELD_SETTING);
expect(yAxisGroup?.items[4]).toEqual(RANGE_MIN_SETTING);
expect(yAxisGroup?.items[5]).toEqual(RANGE_MAX_SETTING);
expect(yAxisGroup?.items[4]).toEqual(CUMULATIVE_SETTING);
expect(yAxisGroup?.items[5]).toEqual(RANGE_MIN_SETTING);
expect(yAxisGroup?.items[6]).toEqual(RANGE_MAX_SETTING);
});
it('should have all expected groups in correct order', () => {
@@ -78,15 +80,16 @@ describe('getBarChartSettings', () => {
);
expect(xAxisGroup).toBeDefined();
expect(xAxisGroup?.items).toHaveLength(6);
expect(xAxisGroup?.items).toHaveLength(7);
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);
expect(xAxisGroup?.items[1]).toEqual(GROUP_BY_SETTING);
expect(xAxisGroup?.items[2]).toEqual(DATE_GRANULARITY_Y_SETTING);
expect(xAxisGroup?.items[3]).toEqual(SORT_BY_GROUP_BY_FIELD_SETTING);
expect(xAxisGroup?.items[4]).toEqual(RANGE_MIN_SETTING);
expect(xAxisGroup?.items[5]).toEqual(RANGE_MAX_SETTING);
expect(xAxisGroup?.items[4]).toEqual(CUMULATIVE_SETTING);
expect(xAxisGroup?.items[5]).toEqual(RANGE_MIN_SETTING);
expect(xAxisGroup?.items[6]).toEqual(RANGE_MAX_SETTING);
});
it('should place PRIMARY axis items under "Y axis" heading', () => {
@@ -0,0 +1,74 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FieldMetadataType } from 'twenty-shared/types';
import { isFieldOrNestedFieldDateKind } from '../isFieldOrNestedFieldDateKind';
describe('isFieldOrNestedFieldDateKind', () => {
it('returns false when fieldId is null', () => {
const result = isFieldOrNestedFieldDateKind({
fieldId: null,
subFieldName: null,
objectMetadataItem: {} as ObjectMetadataItem,
});
expect(result).toBe(false);
});
it('returns true for a DATE type field', () => {
const objectMetadataItem = {
fields: [{ id: 'date-field-id', type: FieldMetadataType.DATE }],
} as ObjectMetadataItem;
const result = isFieldOrNestedFieldDateKind({
fieldId: 'date-field-id',
subFieldName: null,
objectMetadataItem,
});
expect(result).toBe(true);
});
it('returns false for a non-date type field', () => {
const objectMetadataItem = {
fields: [{ id: 'text-field-id', type: FieldMetadataType.TEXT }],
} as ObjectMetadataItem;
const result = isFieldOrNestedFieldDateKind({
fieldId: 'text-field-id',
subFieldName: null,
objectMetadataItem,
});
expect(result).toBe(false);
});
it('returns true for a relation subfield that is a date type', () => {
const objectMetadataItem = {
fields: [
{
id: 'relation-field-id',
type: FieldMetadataType.RELATION,
relation: { targetObjectMetadata: { nameSingular: 'company' } },
},
],
} as ObjectMetadataItem;
const companyObjectMetadataItem = {
nameSingular: 'company',
fields: [
{
name: 'createdAt',
type: FieldMetadataType.DATE,
},
],
} as ObjectMetadataItem;
const result = isFieldOrNestedFieldDateKind({
fieldId: 'relation-field-id',
subFieldName: 'createdAt',
objectMetadataItem,
objectMetadataItems: [companyObjectMetadataItem],
});
expect(result).toBe(true);
});
});
@@ -1,4 +1,6 @@
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
import { isFieldOrNestedFieldDateKind } from '@/command-menu/pages/page-layout/utils/isFieldOrNestedFieldDateKind';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { BarChartGroupMode, GraphOrderBy } from '~/generated/graphql';
@@ -9,6 +11,8 @@ type BuildChartGroupByFieldConfigUpdateArgs<T extends ChartConfiguration> = {
subFieldNameKey: keyof T;
fieldId: string | null;
subFieldName: string | null;
objectMetadataItem?: ObjectMetadataItem;
objectMetadataItems?: ObjectMetadataItem[];
};
export const buildChartGroupByFieldConfigUpdate = <
@@ -19,6 +23,8 @@ export const buildChartGroupByFieldConfigUpdate = <
subFieldNameKey,
fieldId,
subFieldName,
objectMetadataItem,
objectMetadataItems,
}: BuildChartGroupByFieldConfigUpdateArgs<T>) => {
const isPrimaryAxis =
fieldMetadataIdKey === 'primaryAxisGroupByFieldMetadataId';
@@ -44,6 +50,19 @@ export const buildChartGroupByFieldConfigUpdate = <
? configuration.primaryAxisDateGranularity
: null;
const isNewFieldDateType = isFieldOrNestedFieldDateKind({
fieldId,
subFieldName,
objectMetadataItem,
objectMetadataItems,
});
const shouldResetCumulative =
isDefined(fieldId) &&
isDefined(objectMetadataItem) &&
!isNewFieldDateType &&
(isBarChart || isLineChart);
return {
...baseConfig,
primaryAxisOrderBy: isDefined(fieldId)
@@ -52,6 +71,7 @@ export const buildChartGroupByFieldConfigUpdate = <
primaryAxisDateGranularity: isDefined(fieldId)
? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY)
: null,
...(shouldResetCumulative ? { isCumulative: false } : {}),
};
}
@@ -2,6 +2,7 @@ import { CHART_SETTINGS_HEADINGS } from '@/command-menu/pages/page-layout/consta
import { AXIS_NAME_SETTING } from '@/command-menu/pages/page-layout/constants/settings/AxisNameSetting';
import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting';
import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting';
import { CUMULATIVE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/CumulativeSetting';
import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting';
import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting';
import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting';
@@ -40,6 +41,7 @@ export const getBarChartSettings = (
GROUP_BY_SETTING,
DATE_GRANULARITY_Y_SETTING,
SORT_BY_GROUP_BY_FIELD_SETTING,
CUMULATIVE_SETTING,
RANGE_MIN_SETTING,
RANGE_MAX_SETTING,
];
@@ -0,0 +1,38 @@
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 { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
export const isFieldOrNestedFieldDateKind = ({
fieldId,
subFieldName,
objectMetadataItem,
objectMetadataItems,
}: {
fieldId: string | null;
subFieldName: string | null;
objectMetadataItem?: ObjectMetadataItem;
objectMetadataItems?: ObjectMetadataItem[];
}): boolean => {
if (!isDefined(fieldId) || !isDefined(objectMetadataItem)) {
return false;
}
const field = objectMetadataItem.fields.find(
(fieldMetadataItem) => fieldMetadataItem.id === fieldId,
);
if (!isDefined(field)) {
return false;
}
if (isFieldRelation(field) && isDefined(subFieldName)) {
return isNestedFieldDateType(
field,
subFieldName,
objectMetadataItems ?? [],
);
}
return isFieldMetadataDateKind(field.type);
};
@@ -90,6 +90,21 @@ export const shouldHideChartSetting = (
}
}
if (item.id === CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE) {
const isBarOrLineChart =
configuration.__typename === 'BarChartConfiguration' ||
configuration.__typename === 'LineChartConfiguration';
if (isBarOrLineChart) {
return shouldHideDateGranularityBasedOnFieldType(
configuration.primaryAxisGroupByFieldMetadataId,
configuration.primaryAxisGroupBySubFieldName,
objectMetadataItem,
objectMetadataItems ?? [],
);
}
}
if (item.id === CHART_CONFIGURATION_SETTING_IDS.SHOW_LEGEND) {
if (configuration.__typename === 'PieChartConfiguration') {
return false;