[Dashboards] - Min Max range on secondary axis bar charts (#15118)

video QA


https://github.com/user-attachments/assets/70c37188-2398-43de-bbf6-5882bb79940a

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
nitin
2025-10-22 14:21:01 +05:30
committed by GitHub
parent c6addc2bb4
commit 226cc9eb74
37 changed files with 1096 additions and 407 deletions
@@ -0,0 +1,92 @@
import { TextInput } from '@/ui/input/components/TextInput';
import styled from '@emotion/styled';
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;
};
const StyledRightAlignedTextInput = styled(TextInput)`
input {
:focus {
color: ${({ theme }) => theme.font.color.primary};
}
color: ${({ theme }) => theme.font.color.tertiary};
text-align: right;
}
`;
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 handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
};
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 (
<StyledRightAlignedTextInput
value={draftValue}
sizeVariant="sm"
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder={placeholder}
noErrorHelper
/>
);
};
@@ -1,60 +1,61 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
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';
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
import { ChartSettingItem } from '@/command-menu/pages/page-layout/components/chart-settings/ChartSettingItem';
import { ChartTypeSelectionSection } from '@/command-menu/pages/page-layout/components/ChartTypeSelectionSection';
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
import { GRAPH_TYPE_TO_CONFIG_TYPENAME } from '@/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename';
import { useChartSettingsValues } from '@/command-menu/pages/page-layout/hooks/useChartSettingsValues';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdateChartSettingInput } from '@/command-menu/pages/page-layout/hooks/useUpdateChartSettingInput';
import { useUpdateChartSettingToggle } from '@/command-menu/pages/page-layout/hooks/useUpdateChartSettingToggle';
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
import {
CHART_CONFIGURATION_SETTING_IDS,
CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP,
} from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { isChartSettingDisabled } from '@/command-menu/pages/page-layout/utils/isChartSettingDisabled';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
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 {
BarChartGroupMode,
type GraphType,
type PageLayoutWidget,
} from '~/generated/graphql';
import { type GraphType, type PageLayoutWidget } from '~/generated/graphql';
export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { openDropdown } = useOpenDropdown();
const { setSelectedItemId } = useSelectableList(
COMMAND_MENU_LIST_SELECTABLE_LIST_ID,
);
if (widget.configuration?.__typename === 'IframeConfiguration') {
throw new Error(t`IframeConfiguration is not supported`);
}
const configuration = widget.configuration as ChartConfiguration;
const currentGraphType = configuration?.graphType;
const { getChartSettingsValues } = useChartSettingsValues({
objectMetadataId: widget.objectMetadataId,
configuration,
});
const currentGraphType = configuration?.graphType;
const { updateChartSettingToggle } = useUpdateChartSettingToggle({
pageLayoutId,
objectMetadataId: widget.objectMetadataId,
configuration,
});
const { updateChartSettingInput } = useUpdateChartSettingInput(pageLayoutId);
const isGroupByEnabled = getChartSettingsValues(
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
);
const handleGraphTypeChange = (graphType: GraphType) => {
updateCurrentWidgetConfig({
@@ -71,14 +72,6 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
const chartSettings = GRAPH_TYPE_INFORMATION[currentGraphType].settings;
const { setSelectedItemId } = useSelectableList(
COMMAND_MENU_LIST_SELECTABLE_LIST_ID,
);
const isGroupByEnabled = getChartSettingsValues(
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
);
return (
<CommandMenuList
commandGroups={[]}
@@ -93,116 +86,45 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
{chartSettings.map((group) => (
<CommandGroup key={group.heading} heading={group.heading}>
{group.items.map((item) => {
const isDisabled =
(!isNonEmptyString(widget.objectMetadataId) &&
(item?.dependsOn?.includes(
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
) ??
false)) ||
(!isGroupByEnabled &&
item?.dependsOn?.includes(
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
));
const handleToggleChange = () => {
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;
const isDisabled = isChartSettingDisabled(
item,
widget.objectMetadataId,
isGroupByEnabled as boolean,
);
const handleItemToggleChange = () => {
setSelectedItemId(item.id);
if (item.id === CHART_CONFIGURATION_SETTING_IDS.STACKED_BARS) {
const isCurrentlyStacked = getChartSettingsValues(item.id);
const newGroupMode = isCurrentlyStacked
? BarChartGroupMode.GROUPED
: BarChartGroupMode.STACKED;
updateCurrentWidgetConfig({
configToUpdate: {
groupMode: newGroupMode,
},
});
} else {
const newValue = !getChartSettingsValues(item.id);
updateCurrentWidgetConfig({
configToUpdate: {
[configKey]: newValue,
},
});
}
updateChartSettingToggle(item.id);
};
const handleDropdownOpen = () => {
const handleItemInputChange = (value: number | null) => {
updateChartSettingInput(item.id, value);
};
const handleItemDropdownOpen = () => {
openDropdown({
dropdownComponentInstanceIdFromProps: item.id,
});
};
const handleFilterSettingsClick = () => {
const handleFilterClick = () => {
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutGraphFilter,
});
};
if (item.id === CHART_CONFIGURATION_SETTING_IDS.FILTER) {
return (
<SelectableListItem
key={item.id}
itemId={item.id}
onEnter={handleFilterSettingsClick}
>
<CommandMenuItem
id={item.id}
label="Filter"
Icon={item.Icon}
hasSubMenu
onClick={handleFilterSettingsClick}
/>
</SelectableListItem>
);
}
return item.isBoolean ? (
<SelectableListItem
return (
<ChartSettingItem
key={item.id}
itemId={item.id}
onEnter={isDisabled ? undefined : handleToggleChange}
>
<CommandMenuItemToggle
LeftIcon={item.Icon}
text={t(item.label)}
id={item.id}
toggled={getChartSettingsValues(item.id) as boolean}
onToggleChange={handleToggleChange}
/>
</SelectableListItem>
) : (
<SelectableListItem
key={item.id}
itemId={item.id}
onEnter={isDisabled ? undefined : handleDropdownOpen}
>
<CommandMenuItemDropdown
Icon={item.Icon}
label={t(item.label)}
id={item.id}
dropdownId={item.id}
dropdownComponents={
<DropdownContent>
{item.DropdownContent && <item.DropdownContent />}
</DropdownContent>
}
dropdownPlacement="bottom-end"
description={getChartSettingsValues(item.id) as string}
contextualTextPosition={'right'}
hasSubMenu
disabled={isDisabled}
/>
</SelectableListItem>
item={item}
isDisabled={isDisabled}
configuration={configuration}
getChartSettingsValues={getChartSettingsValues}
onToggleChange={handleItemToggleChange}
onInputChange={handleItemInputChange}
onDropdownOpen={handleItemDropdownOpen}
onFilterClick={handleFilterClick}
/>
);
})}
</CommandGroup>
@@ -0,0 +1,132 @@
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 { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
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 { isMinMaxRangeValid } from '@/command-menu/pages/page-layout/utils/isMinMaxRangeValid';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { t } from '@lingui/core/macro';
import { isString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
type ChartSettingItemProps = {
item: ChartSettingsItem;
isDisabled: boolean;
configuration: ChartConfiguration;
getChartSettingsValues: (
itemId: CHART_CONFIGURATION_SETTING_IDS,
) => boolean | string | undefined;
onToggleChange: () => void;
onInputChange: (value: number | null) => void;
onDropdownOpen: () => void;
onFilterClick: () => void;
};
export const ChartSettingItem = ({
item,
isDisabled,
configuration,
getChartSettingsValues,
onToggleChange,
onInputChange,
onDropdownOpen,
onFilterClick,
}: ChartSettingItemProps) => {
if (item.id === CHART_CONFIGURATION_SETTING_IDS.FILTER) {
return (
<SelectableListItem
key={item.id}
itemId={item.id}
onEnter={onFilterClick}
>
<CommandMenuItem
id={item.id}
label={t(item.label)}
Icon={item.Icon}
hasSubMenu
onClick={onFilterClick}
/>
</SelectableListItem>
);
}
if (isDefined(item.isInput)) {
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={onInputChange}
onValidate={(value) =>
isDefined(value) &&
isMinMaxRangeValid(
item.id as
| CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE
| CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE,
value,
configuration,
)
}
placeholder={
item.inputPlaceholder ? t(item.inputPlaceholder) : undefined
}
/>
}
/>
</SelectableListItem>
);
}
if (item.isBoolean) {
return (
<SelectableListItem
key={item.id}
itemId={item.id}
onEnter={isDisabled ? undefined : onToggleChange}
>
<CommandMenuItemToggle
LeftIcon={item.Icon}
text={t(item.label)}
id={item.id}
toggled={getChartSettingsValues(item.id) as boolean}
onToggleChange={onToggleChange}
/>
</SelectableListItem>
);
}
return (
<SelectableListItem
key={item.id}
itemId={item.id}
onEnter={isDisabled ? undefined : onDropdownOpen}
>
<CommandMenuItemDropdown
Icon={item.Icon}
label={t(item.label)}
id={item.id}
dropdownId={item.id}
dropdownComponents={
<DropdownContent>
{item.DropdownContent && <item.DropdownContent />}
</DropdownContent>
}
dropdownPlacement="bottom-end"
description={getChartSettingsValues(item.id) as string}
contextualTextPosition={'right'}
hasSubMenu
disabled={isDisabled}
/>
</SelectableListItem>
);
};
@@ -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';
@@ -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,
],
},
{
@@ -16,4 +16,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`,
};
@@ -0,0 +1,14 @@
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 { msg } from '@lingui/core/macro';
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,
inputPlaceholder: msg`Max`,
};
@@ -0,0 +1,14 @@
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 { msg } from '@lingui/core/macro';
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,
inputPlaceholder: msg`Min`,
};
@@ -160,6 +160,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 '';
}
@@ -0,0 +1,23 @@
import { type CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { getConfigKeyFromSettingId } from '@/command-menu/pages/page-layout/utils/getConfigKeyFromSettingId';
import { useUpdateCurrentWidgetConfig } from './useUpdateCurrentWidgetConfig';
export const useUpdateChartSettingInput = (pageLayoutId: string) => {
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const updateChartSettingInput = (
settingId: CHART_CONFIGURATION_SETTING_IDS,
value: number | null,
) => {
const configKey = getConfigKeyFromSettingId(settingId);
updateCurrentWidgetConfig({
configToUpdate: {
[configKey]: value,
},
});
};
return { updateChartSettingInput };
};
@@ -0,0 +1,53 @@
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { getConfigKeyFromSettingId } from '@/command-menu/pages/page-layout/utils/getConfigKeyFromSettingId';
import { BarChartGroupMode } from '~/generated/graphql';
import { useChartSettingsValues } from './useChartSettingsValues';
import { useUpdateCurrentWidgetConfig } from './useUpdateCurrentWidgetConfig';
export const useUpdateChartSettingToggle = ({
pageLayoutId,
objectMetadataId,
configuration,
}: {
pageLayoutId: string;
objectMetadataId: string;
configuration: ChartConfiguration;
}) => {
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { getChartSettingsValues } = useChartSettingsValues({
objectMetadataId,
configuration,
});
const updateChartSettingToggle = (
settingId: CHART_CONFIGURATION_SETTING_IDS,
) => {
const configKey = getConfigKeyFromSettingId(settingId);
if (settingId === CHART_CONFIGURATION_SETTING_IDS.STACKED_BARS) {
const isCurrentlyStacked = getChartSettingsValues(settingId);
const newGroupMode = isCurrentlyStacked
? BarChartGroupMode.GROUPED
: BarChartGroupMode.STACKED;
updateCurrentWidgetConfig({
configToUpdate: {
groupMode: newGroupMode,
},
});
return;
}
const newValue = !getChartSettingsValues(settingId);
updateCurrentWidgetConfig({
configToUpdate: {
[configKey]: newValue,
},
});
};
return { updateChartSettingToggle };
};
@@ -15,10 +15,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;
@@ -16,4 +16,6 @@ export type ChartSettingsItem = {
isBoolean: boolean;
dependsOn?: CHART_CONFIGURATION_SETTING_IDS[];
DropdownContent?: ComponentType;
isInput?: boolean;
inputPlaceholder?: MessageDescriptor;
};
@@ -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';
@@ -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', () => {
@@ -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', () => {
@@ -0,0 +1,179 @@
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 { msg } from '@lingui/core/macro';
import { IconChartBar } from 'twenty-ui/display';
import { isChartSettingDisabled } from '../isChartSettingDisabled';
describe('isChartSettingDisabled', () => {
const mockItemWithoutDependencies: ChartSettingsItem = {
id: CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS,
label: msg`Data Labels`,
Icon: IconChartBar,
isBoolean: true,
isInput: false,
};
const mockItemDependingOnSource: ChartSettingsItem = {
id: CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
label: msg`X Axis Data`,
Icon: IconChartBar,
isBoolean: false,
isInput: false,
dependsOn: [CHART_CONFIGURATION_SETTING_IDS.SOURCE],
};
const mockItemDependingOnGroupBy: ChartSettingsItem = {
id: CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD,
label: msg`Sort By Group`,
Icon: IconChartBar,
isBoolean: false,
isInput: false,
dependsOn: [CHART_CONFIGURATION_SETTING_IDS.GROUP_BY],
};
const mockItemWithMultipleDependencies: ChartSettingsItem = {
id: CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
label: msg`X Axis Data`,
Icon: IconChartBar,
isBoolean: false,
isInput: false,
dependsOn: [
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
],
};
describe('item without dependencies', () => {
it('should return false when item has no dependencies', () => {
const result = isChartSettingDisabled(
mockItemWithoutDependencies,
'valid-object-id',
true,
);
expect(result).toBe(false);
});
it('should return false when item has no dependencies even with no object metadata', () => {
const result = isChartSettingDisabled(
mockItemWithoutDependencies,
'',
false,
);
expect(result).toBe(false);
});
});
describe('item depending on SOURCE', () => {
it('should return true when no object metadata and item depends on SOURCE', () => {
const result = isChartSettingDisabled(
mockItemDependingOnSource,
'',
true,
);
expect(result).toBe(true);
});
it('should return false when object metadata exists and item depends on SOURCE', () => {
const result = isChartSettingDisabled(
mockItemDependingOnSource,
'valid-object-id',
true,
);
expect(result).toBe(false);
});
});
describe('item depending on GROUP_BY', () => {
it('should return true when group by is not enabled and item depends on GROUP_BY', () => {
const result = isChartSettingDisabled(
mockItemDependingOnGroupBy,
'valid-object-id',
false,
);
expect(result).toBe(true);
});
it('should return false when group by is enabled and item depends on GROUP_BY', () => {
const result = isChartSettingDisabled(
mockItemDependingOnGroupBy,
'valid-object-id',
true,
);
expect(result).toBe(false);
});
});
describe('item with multiple dependencies', () => {
it('should return true if any dependency is not met (no object metadata)', () => {
const result = isChartSettingDisabled(
mockItemWithMultipleDependencies,
'',
true,
);
expect(result).toBe(true);
});
it('should return true if any dependency is not met (group by disabled)', () => {
const result = isChartSettingDisabled(
mockItemWithMultipleDependencies,
'valid-object-id',
false,
);
expect(result).toBe(true);
});
it('should return false if all dependencies are met', () => {
const result = isChartSettingDisabled(
mockItemWithMultipleDependencies,
'valid-object-id',
true,
);
expect(result).toBe(false);
});
});
describe('edge cases', () => {
it('should handle undefined dependsOn array', () => {
const itemWithUndefinedDependsOn: ChartSettingsItem = {
id: CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS,
label: msg`Data Labels`,
Icon: IconChartBar,
isBoolean: true,
isInput: false,
dependsOn: undefined,
};
const result = isChartSettingDisabled(
itemWithUndefinedDependsOn,
'',
false,
);
expect(result).toBe(false);
});
it('should handle empty dependsOn array', () => {
const itemWithEmptyDependsOn: ChartSettingsItem = {
id: CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS,
label: msg`Data Labels`,
Icon: IconChartBar,
isBoolean: true,
isInput: false,
dependsOn: [],
};
const result = isChartSettingDisabled(itemWithEmptyDependsOn, '', false);
expect(result).toBe(false);
});
});
});
@@ -0,0 +1,117 @@
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { isMinMaxRangeValid } from '@/command-menu/pages/page-layout/utils/isMinMaxRangeValid';
import { GraphType } from '~/generated/graphql';
describe('isMinMaxRangeValid', () => {
const mockConfiguration = {
__typename: 'BarChartConfiguration',
graphType: GraphType.VERTICAL_BAR,
rangeMin: 10,
rangeMax: 100,
} as ChartConfiguration;
describe('MIN_RANGE validation', () => {
it('should be valid when new rangeMin is less than existing rangeMax', () => {
const result = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE,
50,
mockConfiguration,
);
expect(result).toBe(true);
});
it('should be valid when new rangeMin equals existing rangeMax', () => {
const result = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE,
100,
mockConfiguration,
);
expect(result).toBe(true);
});
it('should be invalid when new rangeMin is greater than existing rangeMax', () => {
const result = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE,
150,
mockConfiguration,
);
expect(result).toBe(false);
});
});
describe('MAX_RANGE validation', () => {
it('should be valid when new rangeMax is greater than existing rangeMin', () => {
const result = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE,
50,
mockConfiguration,
);
expect(result).toBe(true);
});
it('should be valid when new rangeMax equals existing rangeMin', () => {
const result = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE,
10,
mockConfiguration,
);
expect(result).toBe(true);
});
it('should be invalid when new rangeMax is less than existing rangeMin', () => {
const result = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE,
5,
mockConfiguration,
);
expect(result).toBe(false);
});
});
describe('edge cases', () => {
it('should be valid when new rangeMin is negative and greater than existing rangeMax', () => {
const configWithNegatives = {
__typename: 'BarChartConfiguration',
graphType: GraphType.VERTICAL_BAR,
rangeMin: -100,
rangeMax: -10,
} as ChartConfiguration;
const validMin = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE,
-50,
configWithNegatives,
);
expect(validMin).toBe(true);
const invalidMin = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE,
-5,
configWithNegatives,
);
expect(invalidMin).toBe(false);
});
it('should be valid when new rangeMin is zero and greater than existing rangeMax', () => {
const configWithZero = {
__typename: 'BarChartConfiguration',
graphType: GraphType.VERTICAL_BAR,
rangeMin: 0,
rangeMax: 100,
} as ChartConfiguration;
const result = isMinMaxRangeValid(
CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE,
0,
configWithZero,
);
expect(result).toBe(true);
});
});
});
@@ -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';
@@ -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;
@@ -0,0 +1,14 @@
import {
type CHART_CONFIGURATION_SETTING_IDS,
CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP,
} from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
export const getConfigKeyFromSettingId = (
settingId: CHART_CONFIGURATION_SETTING_IDS,
): string => {
return settingId in CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP
? CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP[
settingId as keyof typeof CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP
]
: settingId;
};
@@ -0,0 +1,22 @@
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 { isNonEmptyString } from '@sniptt/guards';
export const isChartSettingDisabled = (
item: ChartSettingsItem,
objectMetadataId: string,
isGroupByEnabled: boolean,
): boolean => {
const hasNoObjectMetadata = !isNonEmptyString(objectMetadataId);
const dependsOnSource = item?.dependsOn?.includes(
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
);
const dependsOnGroupBy = item?.dependsOn?.includes(
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
);
return (
(hasNoObjectMetadata && (dependsOnSource ?? false)) ||
(!isGroupByEnabled && (dependsOnGroupBy ?? false))
);
};
@@ -0,0 +1,35 @@
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds';
import { isDefined } from 'twenty-shared/utils';
export const isMinMaxRangeValid = (
settingId:
| CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE
| CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE,
newValue: number,
configuration: ChartConfiguration,
): boolean => {
if (!('rangeMax' in configuration || 'rangeMin' in configuration)) {
return false;
}
if (settingId === CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE) {
if (
isDefined(configuration.rangeMax) &&
newValue > configuration.rangeMax
) {
return false;
}
}
if (settingId === CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE) {
if (
isDefined(configuration.rangeMin) &&
newValue < configuration.rangeMin
) {
return false;
}
}
return true;
};