Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9882c6b
done
ehconitin Oct 15, 2025
bff3f99
range should be only for numeric values, like omit null values
ehconitin Oct 16, 2025
808832c
improve input
ehconitin Oct 16, 2025
4a5db13
validate range max min and make groupby resolver service changes more…
ehconitin Oct 16, 2025
ddb770b
grep
ehconitin Oct 16, 2025
a9c02d6
update snapshot
ehconitin Oct 16, 2025
5031066
fix
ehconitin Oct 16, 2025
27f2475
fix
ehconitin Oct 16, 2025
871e2ea
improve
ehconitin Oct 16, 2025
549b184
nit
ehconitin Oct 16, 2025
58ab7f0
macros adjust
ehconitin Oct 16, 2025
a62ce9b
figma
ehconitin Oct 16, 2025
3f46f4a
Merge remote-tracking branch 'upstream/main' into min-max-range-clean
ehconitin Oct 16, 2025
138d3e7
improve
ehconitin Oct 16, 2025
f94c950
new plan
ehconitin Oct 16, 2025
eca043b
continuew
ehconitin Oct 16, 2025
0983d71
Merge remote-tracking branch 'upstream/main' into min-max-range-clean
ehconitin Oct 16, 2025
dbe0da7
fix
ehconitin Oct 16, 2025
9512d6d
improve
ehconitin Oct 17, 2025
7d9df2c
match figma
ehconitin Oct 17, 2025
adf8d1c
Merge remote-tracking branch 'upstream/main' into min-max-range-clean
ehconitin Oct 17, 2025
e36a07f
Merge remote-tracking branch 'upstream/main' into min-max-range-clean
ehconitin Oct 17, 2025
5d8295c
refact
ehconitin Oct 17, 2025
eaf6f06
Merge remote-tracking branch 'upstream/main' into min-max-range-clean
ehconitin Oct 19, 2025
40fe7fc
Merge branch 'main' into min-max-range
charlesBochet Oct 21, 2025
828d689
Merge branch 'main' into min-max-range
charlesBochet Oct 22, 2025
9bc5100
Fixes
charlesBochet Oct 22, 2025
d707db7
Merge branch 'main' into min-max-range
charlesBochet Oct 22, 2025
bffe841
Fixes
charlesBochet Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { TextInput } from '@/ui/input/components/TextInput';
import { useState } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import {
canBeCastAsNumberOrNull,
castAsNumberOrNull,
} from '~/utils/cast-as-number-or-null';

type CommandMenuItemNumberInputProps = {
value: string;
onChange: (value: number | null) => void;
onValidate?: (value: number | null) => boolean;
placeholder?: string;
};

export const CommandMenuItemNumberInput = ({
value,
onChange,
onValidate,
placeholder,
}: CommandMenuItemNumberInputProps) => {
const [draftValue, setDraftValue] = useState(value);
const [hasError, setHasError] = useState(false);

const handleChange = (text: string) => {
setDraftValue(text);
if (hasError) {
setHasError(false);
}
};

const handleCommit = () => {
if (!canBeCastAsNumberOrNull(draftValue)) {
setHasError(true);
setDraftValue(value);
return;
}

const numericValue = castAsNumberOrNull(draftValue);

if (isDefined(onValidate)) {
const isInvalid = onValidate(numericValue);
if (isInvalid) {
setHasError(true);
return;
}
}

onChange(numericValue);
setHasError(false);
};

const handleBlur = () => {
handleCommit();
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === Key.Enter || event.key === Key.Escape) {
event.stopPropagation();
handleCommit();
} else {
event.stopPropagation();
}
};

return (
<TextInput
type="number"
value={draftValue}
sizeVariant="sm"
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder={placeholder}
error={hasError ? ' ' : undefined}
noErrorHelper
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
import { CommandMenuItemNumberInput } from '@/command-menu/components/CommandMenuItemNumberInput';
import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItemToggle';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { COMMAND_MENU_LIST_SELECTABLE_LIST_ID } from '@/command-menu/constants/CommandMenuListSelectableListId';
Expand All @@ -23,7 +24,8 @@ import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { isNonEmptyString, isString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { IconFilter } from 'twenty-ui/display';

import {
Expand Down Expand Up @@ -151,6 +153,56 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
});
};

const createRangeValidator = (itemId: string) => {
return (value: number | null): boolean => {
const configKey =
itemId in CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP
? CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP[
itemId as keyof typeof CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP
]
: itemId;

if (configKey === 'rangeMin') {
if (
'rangeMax' in configuration &&
isDefined(configuration.rangeMax) &&
isDefined(value) &&
value > configuration.rangeMax
) {
return true;
}
}

if (configKey === 'rangeMax') {
if (
'rangeMin' in configuration &&
isDefined(configuration.rangeMin) &&
isDefined(value) &&
value < configuration.rangeMin
) {
return true;
}
}

return false;
};
};

const handleInputChange = (value: number | null) => {
const configKey =
item.id in CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP
? CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP[
item.id as keyof typeof CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP
]
: item.id;

updateCurrentWidgetConfig({
configToUpdate: {
[configKey]: value,
},
});
};

if (item.id === CHART_CONFIGURATION_SETTING_IDS.FILTER) {
return (
<SelectableListItem
Expand All @@ -160,7 +212,7 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
>
<CommandMenuItem
id={item.id}
label="Filter"
label={t(item.label)}
Icon={item.Icon}
hasSubMenu
onClick={handleFilterSettingsClick}
Expand All @@ -169,6 +221,29 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
);
}

if (item.isInput === true) {
const settingValue = getChartSettingsValues(item.id);
const stringValue = isString(settingValue) ? settingValue : '';

return (
<SelectableListItem key={item.id} itemId={item.id}>
<CommandMenuItem
id={item.id}
label={t(item.label)}
Icon={item.Icon}
RightComponent={
<CommandMenuItemNumberInput
value={stringValue}
onChange={handleInputChange}
onValidate={createRangeValidator(item.id)}
placeholder={t`Auto`}
/>
}
/>
</SelectableListItem>
);
}

return item.isBoolean ? (
<SelectableListItem
key={item.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/
import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting';
import { GROUP_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/GroupBySetting';
import { OMIT_NULL_VALUES_SETTING } from '@/command-menu/pages/page-layout/constants/settings/OmitNullValuesSetting';
import { RANGE_MAX_SETTING } from '@/command-menu/pages/page-layout/constants/settings/RangeMaxSetting';
import { RANGE_MIN_SETTING } from '@/command-menu/pages/page-layout/constants/settings/RangeMinSetting';
import { SORT_BY_GROUP_BY_FIELD_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting';
import { SORT_BY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByXSetting';
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
Expand All @@ -30,6 +32,8 @@ export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [
DATA_DISPLAY_Y_SETTING,
GROUP_BY_SETTING,
SORT_BY_GROUP_BY_FIELD_SETTING,
RANGE_MIN_SETTING,
RANGE_MAX_SETTING,
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ export const CHART_CONFIGURATION_SETTING_LABELS = {
AXIS_NAME: msg`Axis name`,
STACKED_BARS: msg`Stacked bars`,
OMIT_NULL_VALUES: msg`Omit zero values`,
MIN_RANGE: msg`Min range`,
MAX_RANGE: msg`Max range`,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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 { IconMathMax } from 'twenty-ui/display';

export const RANGE_MAX_SETTING: ChartSettingsItem = {
isBoolean: false,
Icon: IconMathMax,
label: CHART_CONFIGURATION_SETTING_LABELS.MAX_RANGE,
id: CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE,
isInput: true,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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 { IconMathMin } from 'twenty-ui/display';

export const RANGE_MIN_SETTING: ChartSettingsItem = {
isBoolean: false,
Icon: IconMathMin,
label: CHART_CONFIGURATION_SETTING_LABELS.MIN_RANGE,
id: CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE,
isInput: true,
};
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ export const useChartSettingsValues = ({
return 'omitNullValues' in configuration
? (configuration.omitNullValues ?? false)
: false;
case CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE:
return 'rangeMin' in configuration
? (configuration.rangeMin?.toString() ?? '')
: '';
case CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE:
return 'rangeMax' in configuration
? (configuration.rangeMax?.toString() ?? '')
: '';
default:
return '';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ export enum CHART_CONFIGURATION_SETTING_IDS {
AXIS_NAME = 'AXIS_NAME',
STACKED_BARS = 'STACKED_BARS',
OMIT_NULL_VALUES = 'OMIT_NULL_VALUES',
MIN_RANGE = 'MIN_RANGE',
MAX_RANGE = 'MAX_RANGE',
}

export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = {
[CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS]: 'displayDataLabel',
[CHART_CONFIGURATION_SETTING_IDS.STACKED_BARS]: 'groupMode',
[CHART_CONFIGURATION_SETTING_IDS.OMIT_NULL_VALUES]: 'omitNullValues',
[CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE]: 'rangeMin',
[CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE]: 'rangeMax',
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export type ChartSettingsItem = {
isBoolean: boolean;
dependsOn?: CHART_CONFIGURATION_SETTING_IDS[];
DropdownContent?: ComponentType;
isInput?: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/
import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting';
import { GROUP_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/GroupBySetting';
import { OMIT_NULL_VALUES_SETTING } from '@/command-menu/pages/page-layout/constants/settings/OmitNullValuesSetting';
import { RANGE_MAX_SETTING } from '@/command-menu/pages/page-layout/constants/settings/RangeMaxSetting';
import { RANGE_MIN_SETTING } from '@/command-menu/pages/page-layout/constants/settings/RangeMinSetting';
import { SORT_BY_GROUP_BY_FIELD_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting';
import { SORT_BY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByXSetting';
import { STACKED_BARS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/StackedBarsSetting';
Expand Down Expand Up @@ -36,12 +38,14 @@ describe('getBarChartSettings', () => {
const yAxisGroup = result.find((group) => group.heading === 'Y axis');

expect(yAxisGroup).toBeDefined();
expect(yAxisGroup?.items).toHaveLength(3);
expect(yAxisGroup?.items).toHaveLength(5);
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(SORT_BY_GROUP_BY_FIELD_SETTING);
expect(yAxisGroup?.items[3]).toEqual(RANGE_MIN_SETTING);
expect(yAxisGroup?.items[4]).toEqual(RANGE_MAX_SETTING);
});

it('should have all expected groups in correct order', () => {
Expand All @@ -62,12 +66,14 @@ describe('getBarChartSettings', () => {
const xAxisGroup = result.find((group) => group.heading === 'X axis');

expect(xAxisGroup).toBeDefined();
expect(xAxisGroup?.items).toHaveLength(3);
expect(xAxisGroup?.items).toHaveLength(5);
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(SORT_BY_GROUP_BY_FIELD_SETTING);
expect(xAxisGroup?.items[3]).toEqual(RANGE_MIN_SETTING);
expect(xAxisGroup?.items[4]).toEqual(RANGE_MAX_SETTING);
});

it('should place PRIMARY axis items under "Y axis" heading', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/
import { FILTER_SETTING } from '@/command-menu/pages/page-layout/constants/settings/FilterSetting';
import { GROUP_BY_SETTING } from '@/command-menu/pages/page-layout/constants/settings/GroupBySetting';
import { OMIT_NULL_VALUES_SETTING } from '@/command-menu/pages/page-layout/constants/settings/OmitNullValuesSetting';
import { RANGE_MAX_SETTING } from '@/command-menu/pages/page-layout/constants/settings/RangeMaxSetting';
import { RANGE_MIN_SETTING } from '@/command-menu/pages/page-layout/constants/settings/RangeMinSetting';
import { SORT_BY_GROUP_BY_FIELD_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByGroupByFieldSetting';
import { SORT_BY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/SortByXSetting';
import { STACKED_BARS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/StackedBarsSetting';
Expand All @@ -32,6 +34,8 @@ export const getBarChartSettings = (
{ ...DATA_DISPLAY_Y_SETTING, Icon: dataDisplayYIcon },
GROUP_BY_SETTING,
SORT_BY_GROUP_BY_FIELD_SETTING,
RANGE_MIN_SETTING,
RANGE_MAX_SETTING,
];

const xAxisItems = isHorizontal ? secondaryAxisItems : primaryAxisItems;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type GraphWidgetBarChartProps = {
layout?: 'vertical' | 'horizontal';
groupMode?: 'grouped' | 'stacked';
seriesLabels?: Record<string, string>;
rangeMin?: number;
rangeMax?: number;
} & GraphValueFormatOptions;

const StyledContainer = styled.div`
Expand All @@ -65,6 +67,8 @@ export const GraphWidgetBarChart = ({
layout = 'vertical',
groupMode = 'grouped',
seriesLabels,
rangeMin,
rangeMax,
displayType,
decimals,
prefix,
Expand Down Expand Up @@ -180,7 +184,12 @@ export const GraphWidgetBarChart = ({
padding={0.3}
groupMode={groupMode}
layout={layout}
valueScale={{ type: 'linear' }}
valueScale={{
type: 'linear',
min: rangeMin ?? 'auto',
max: rangeMax ?? 'auto',
clamp: true,
}}
indexScale={{ type: 'band', round: true }}
colors={(datum) => getBarChartColor(datum, barConfigs, theme)}
defs={defs}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export const GraphWidgetBarChartRenderer = ({
groupMode={groupMode}
id={widget.id}
displayType="shortNumber"
rangeMin={configuration.rangeMin ?? undefined}
rangeMax={configuration.rangeMax ?? undefined}
/>
</Suspense>
);
Expand Down
Loading
Loading