Split bar graph into two distinct horizontal and vertical bars (#15061)
### Summary Split BAR into VERTICAL_BAR and HORIZONTAL_BAR as separate chart types. ### The Problem Initially wanted a simple vertical/horizontal toggle for bar charts, but ran into a GraphQL union type constraint: union types can't have the same field name with different nullability. - Vertical bars need groupByFieldMetadataIdX as required (categories on X) - Horizontal bars need groupByFieldMetadataIdY as required (categories on Y) - GraphQL schema generation fails with this setup ### The Solution Use semantic primaryAxis and secondaryAxis naming that's orientation-agnostic: - primaryAxisGroupByFieldMetadataId = main grouping field (e.g., "Company Name") - secondaryAxisGroupByFieldMetadataId = optional secondary grouping (e.g., "Stage") These fields have consistent meaning regardless of orientation. The visual mapping happens at the UI layer: - Vertical bars: primary data renders on X-axis, secondary on Y-axis - Horizontal bars: primary data renders on Y-axis, secondary on X-axis Both chart types share the same DTO structure with consistent nullability. ### What Changed - Split GraphType.BAR → VERTICAL_BAR | HORIZONTAL_BAR - Renamed fields: primaryAxisGroupByFieldMetadataId, secondaryAxisGroupByFieldMetadataId (+ subfield variants) - useChartSettingsValues(): Maps semantic fields to setting values (no swapping) - getBarChartSettings(): Dynamically arranges settings panel based on orientation - transformGroupByDataToBarChartData(): Maps semantic fields to Nivo's layout prop video QA https://github.com/user-attachments/assets/479061b5-712e-4ca6-9858-95273d1f16c1
This commit is contained in:
+2
-1
@@ -6,7 +6,8 @@ import { t } from '@lingui/core/macro';
|
||||
import { MenuPicker } from 'twenty-ui/navigation';
|
||||
|
||||
const graphTypeOptions = [
|
||||
GraphType.BAR,
|
||||
GraphType.VERTICAL_BAR,
|
||||
GraphType.HORIZONTAL_BAR,
|
||||
GraphType.NUMBER,
|
||||
GraphType.PIE,
|
||||
GraphType.LINE,
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
const fieldSelection = buildBarChartFieldSelection();
|
||||
const newWidget = createPageLayoutGraphWidget({
|
||||
graphType: GraphType.BAR,
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
fieldSelection,
|
||||
});
|
||||
|
||||
|
||||
+6
-4
@@ -86,10 +86,12 @@ export const ChartDataSourceDropdownContent = () => {
|
||||
objectMetadataId,
|
||||
configToUpdate: {
|
||||
aggregateFieldMetadataId: null,
|
||||
groupByFieldMetadataIdX: null,
|
||||
groupByFieldMetadataIdY: null,
|
||||
groupBySubFieldNameX: null,
|
||||
groupBySubFieldNameY: null,
|
||||
primaryAxisGroupByFieldMetadataId: null,
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
secondaryAxisGroupByFieldMetadataId: null,
|
||||
secondaryAxisGroupBySubFieldName: null,
|
||||
primaryAxisOrderBy: null,
|
||||
secondaryAxisOrderBy: null,
|
||||
groupByFieldMetadataId: null,
|
||||
groupBySubFieldName: null,
|
||||
},
|
||||
|
||||
+7
-10
@@ -14,7 +14,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import { isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
@@ -27,22 +27,23 @@ export const ChartFieldSelectionForAggregateOperationDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const currentFieldMetadataId =
|
||||
widgetInEditMode.configuration.groupByFieldMetadataIdY;
|
||||
const currentFieldMetadataId = configuration.aggregateFieldMetadataId;
|
||||
|
||||
const [selectedFieldMetadataId, setSelectedFieldMetadataId] = useState(
|
||||
currentFieldMetadataId,
|
||||
);
|
||||
|
||||
const sourceObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
(item) => item.id === widgetInEditMode?.objectMetadataId,
|
||||
);
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
@@ -68,10 +69,6 @@ export const ChartFieldSelectionForAggregateOperationDropdownContent = () => {
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
if (!isDefined(sourceObjectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isSubMenuOpen) {
|
||||
return (
|
||||
<ChartAggregateOperationSelectionDropdownContent
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ export const ChartGroupByFieldSelectionDropdownContent = () => {
|
||||
<ChartGroupByFieldSelectionDropdownContentBase<
|
||||
BarChartConfiguration | LineChartConfiguration
|
||||
>
|
||||
fieldMetadataIdKey="groupByFieldMetadataIdY"
|
||||
subFieldNameKey="groupBySubFieldNameY"
|
||||
fieldMetadataIdKey="secondaryAxisGroupByFieldMetadataId"
|
||||
subFieldNameKey="secondaryAxisGroupBySubFieldName"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+13
-18
@@ -13,21 +13,20 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type GraphOrderBy,
|
||||
type LineChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
import { type GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const ChartSortByGroupByFieldDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const configuration = widgetInEditMode?.configuration as
|
||||
| BarChartConfiguration
|
||||
| LineChartConfiguration;
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
|
||||
const currentOrderBy = configuration.orderByY;
|
||||
if (
|
||||
configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
@@ -45,9 +44,7 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
|
||||
|
||||
const handleSelectSortOption = (orderBy: GraphOrderBy) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
orderByY: orderBy,
|
||||
},
|
||||
configToUpdate: { secondaryAxisOrderBy: orderBy },
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
@@ -78,13 +75,11 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
|
||||
text={getGroupBySortOptionLabel({
|
||||
graphOrderBy: sortOption.value,
|
||||
groupByFieldMetadataId:
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? configuration.groupByFieldMetadataIdY
|
||||
: 'groupByFieldMetadataId' in configuration
|
||||
? configuration.groupByFieldMetadataId
|
||||
: undefined,
|
||||
configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
})}
|
||||
selected={currentOrderBy === sortOption.value}
|
||||
selected={
|
||||
configuration.secondaryAxisOrderBy === sortOption.value
|
||||
}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={sortOption.icon}
|
||||
onClick={() => {
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ export const ChartXAxisFieldSelectionDropdownContent = () => {
|
||||
<ChartGroupByFieldSelectionDropdownContentBase<
|
||||
BarChartConfiguration | LineChartConfiguration
|
||||
>
|
||||
fieldMetadataIdKey="groupByFieldMetadataIdX"
|
||||
subFieldNameKey="groupBySubFieldNameX"
|
||||
fieldMetadataIdKey="primaryAxisGroupByFieldMetadataId"
|
||||
subFieldNameKey="primaryAxisGroupBySubFieldName"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+20
-20
@@ -11,24 +11,22 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { type GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const ChartXAxisSortBySelectionDropdownContent = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
|
||||
if (
|
||||
widgetInEditMode?.configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
widgetInEditMode?.configuration?.__typename !== 'LineChartConfiguration'
|
||||
configuration?.__typename !== 'BarChartConfiguration' &&
|
||||
configuration?.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
throw new Error('Invalid configuration type');
|
||||
}
|
||||
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
const currentOrderByX = configuration.orderByX;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
@@ -40,22 +38,19 @@ export const ChartXAxisSortBySelectionDropdownContent = () => {
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleSelectSortOption = (orderByX: GraphOrderBy) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
orderByX,
|
||||
},
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const { getXSortOptionLabel } = useGraphXSortOptionLabels({
|
||||
objectMetadataId: widgetInEditMode?.objectMetadataId,
|
||||
});
|
||||
|
||||
const handleSelect = (orderBy: GraphOrderBy) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: { primaryAxisOrderBy: orderBy },
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
@@ -68,23 +63,28 @@ export const ChartXAxisSortBySelectionDropdownContent = () => {
|
||||
key={sortOption.value}
|
||||
itemId={sortOption.value}
|
||||
onEnter={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
handleSelect(sortOption.value);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getXSortOptionLabel({
|
||||
graphOrderBy: sortOption.value,
|
||||
groupByFieldMetadataIdX: configuration.groupByFieldMetadataIdX,
|
||||
groupByFieldMetadataIdX:
|
||||
configuration.primaryAxisGroupByFieldMetadataId,
|
||||
groupBySubFieldNameX:
|
||||
configuration.primaryAxisGroupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined,
|
||||
aggregateFieldMetadataId:
|
||||
configuration.aggregateFieldMetadataId ?? undefined,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation ?? undefined,
|
||||
})}
|
||||
selected={currentOrderByX === sortOption.value}
|
||||
selected={configuration.primaryAxisOrderBy === sortOption.value}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={sortOption.icon}
|
||||
onClick={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
handleSelect(sortOption.value);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
|
||||
+10
-4
@@ -1,14 +1,15 @@
|
||||
import { BAR_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/BarChartSettings';
|
||||
import { GAUGE_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/GaugeChartSettings';
|
||||
import { LINE_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/LineChartSettings';
|
||||
import { NUMBER_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/NumberChartSettings';
|
||||
import { PIE_CHART_SETTINGS } from '@/command-menu/pages/page-layout/constants/PieChartSettings';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { getBarChartSettings } from '@/command-menu/pages/page-layout/utils/getBarChartSettings';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
Icon123,
|
||||
IconChartBar,
|
||||
IconChartBarHorizontal,
|
||||
IconChartLine,
|
||||
IconChartPie,
|
||||
type IconComponent,
|
||||
@@ -24,10 +25,15 @@ export const GRAPH_TYPE_INFORMATION: Record<
|
||||
settings: ChartSettingsGroup[];
|
||||
}
|
||||
> = {
|
||||
[GraphType.BAR]: {
|
||||
label: msg`Bar`,
|
||||
[GraphType.VERTICAL_BAR]: {
|
||||
label: msg`Vertical Bar`,
|
||||
icon: IconChartBar,
|
||||
settings: BAR_CHART_SETTINGS,
|
||||
settings: getBarChartSettings(GraphType.VERTICAL_BAR),
|
||||
},
|
||||
[GraphType.HORIZONTAL_BAR]: {
|
||||
label: msg`Horizontal Bar`,
|
||||
icon: IconChartBarHorizontal,
|
||||
settings: getBarChartSettings(GraphType.HORIZONTAL_BAR),
|
||||
},
|
||||
[GraphType.PIE]: {
|
||||
label: msg`Pie`,
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const GRAPH_TYPE_TO_CONFIG_TYPENAME = {
|
||||
[GraphType.BAR]: 'BarChartConfiguration',
|
||||
[GraphType.VERTICAL_BAR]: 'BarChartConfiguration',
|
||||
[GraphType.HORIZONTAL_BAR]: 'BarChartConfiguration',
|
||||
[GraphType.LINE]: 'LineChartConfiguration',
|
||||
[GraphType.PIE]: 'PieChartConfiguration',
|
||||
[GraphType.NUMBER]: 'NumberChartConfiguration',
|
||||
|
||||
+592
@@ -0,0 +1,592 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
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 { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
AxisNameDisplay,
|
||||
type BarChartConfiguration,
|
||||
BarChartGroupMode,
|
||||
ExtendedAggregateOperations,
|
||||
FieldMetadataType,
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useChartSettingsValues } from '../useChartSettingsValues';
|
||||
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
labelSingular: 'Company',
|
||||
labelPlural: 'Companies',
|
||||
fields: [
|
||||
{
|
||||
id: 'field-company-name',
|
||||
name: 'name',
|
||||
label: 'Company Name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
id: 'field-amount',
|
||||
name: 'amount',
|
||||
label: 'Amount',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
},
|
||||
{
|
||||
id: 'field-stage',
|
||||
name: 'stage',
|
||||
label: 'Stage',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
id: 'field-full-name',
|
||||
name: 'fullName',
|
||||
label: 'Full Name',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
subFields: [
|
||||
{
|
||||
id: 'field-full-name-first',
|
||||
name: 'firstName',
|
||||
label: 'First Name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const buildBarChartConfiguration = (
|
||||
overrides: Partial<BarChartConfiguration>,
|
||||
): BarChartConfiguration =>
|
||||
({
|
||||
__typename: 'BarChartConfiguration',
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
aggregateOperation: ExtendedAggregateOperations.SUM,
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-company-name',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
secondaryAxisGroupByFieldMetadataId: null,
|
||||
secondaryAxisGroupBySubFieldName: null,
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
secondaryAxisOrderBy: null,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
displayDataLabel: false,
|
||||
groupMode: 'GROUPED',
|
||||
...overrides,
|
||||
}) as BarChartConfiguration;
|
||||
|
||||
const renderUseChartSettingsValues = (configuration: ChartConfiguration) => {
|
||||
return renderHook(
|
||||
() =>
|
||||
useChartSettingsValues({
|
||||
objectMetadataId: mockObjectMetadataItem.id,
|
||||
configuration,
|
||||
}),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<RecoilRoot
|
||||
initializeState={({ set }) => {
|
||||
set(objectMetadataItemsState, [mockObjectMetadataItem]);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecoilRoot>
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe('useChartSettingsValues', () => {
|
||||
describe('Vertical bar chart (1D - single grouping)', () => {
|
||||
const verticalBarConfig = buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-company-name',
|
||||
secondaryAxisGroupByFieldMetadataId: null,
|
||||
});
|
||||
|
||||
it('should return primary field label for DATA_ON_DISPLAY_X', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
expect(value).toBe('Company Name');
|
||||
});
|
||||
|
||||
it('should return aggregate field with operation for DATA_ON_DISPLAY_Y', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y,
|
||||
);
|
||||
|
||||
expect(value).toBe('Amount (Sum)');
|
||||
});
|
||||
|
||||
it('should return object label plural for SOURCE', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
|
||||
);
|
||||
|
||||
expect(value).toBe('Companies');
|
||||
});
|
||||
|
||||
it('should return sort label for SORT_BY_X', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X,
|
||||
);
|
||||
|
||||
expect(value).toBeDefined();
|
||||
expect(typeof value).toBe('string');
|
||||
});
|
||||
|
||||
it('should return displayDataLabel value for DATA_LABELS', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS,
|
||||
);
|
||||
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
|
||||
it('should return axis name display option for AXIS_NAME', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME,
|
||||
);
|
||||
|
||||
expect(value).toBeDefined();
|
||||
expect(typeof value).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Horizontal bar chart (1D - single grouping)', () => {
|
||||
const horizontalBarConfig = buildBarChartConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-company-name',
|
||||
secondaryAxisGroupByFieldMetadataId: null,
|
||||
});
|
||||
|
||||
it('should return SAME primary field label for DATA_ON_DISPLAY_X (no swapping)', () => {
|
||||
const { result } = renderUseChartSettingsValues(horizontalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
// Critical test: horizontal should return the SAME value as vertical
|
||||
expect(value).toBe('Company Name');
|
||||
});
|
||||
|
||||
it('should return SAME aggregate field for DATA_ON_DISPLAY_Y (no swapping)', () => {
|
||||
const { result } = renderUseChartSettingsValues(horizontalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y,
|
||||
);
|
||||
|
||||
// Critical test: horizontal should return the SAME value as vertical
|
||||
expect(value).toBe('Amount (Sum)');
|
||||
});
|
||||
|
||||
it('should return SAME sort label for SORT_BY_X (no swapping)', () => {
|
||||
const { result } = renderUseChartSettingsValues(horizontalBarConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X,
|
||||
);
|
||||
|
||||
expect(value).toBeDefined();
|
||||
expect(typeof value).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vertical bar chart (2D - with secondary grouping)', () => {
|
||||
const verticalBar2DConfig = buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-company-name',
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-stage',
|
||||
secondaryAxisOrderBy: GraphOrderBy.FIELD_DESC,
|
||||
});
|
||||
|
||||
it('should return primary field label for DATA_ON_DISPLAY_X', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBar2DConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
expect(value).toBe('Company Name');
|
||||
});
|
||||
|
||||
it('should return secondary field label for GROUP_BY', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBar2DConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
|
||||
);
|
||||
|
||||
expect(value).toBe('Stage');
|
||||
});
|
||||
|
||||
it('should return secondary sort label for SORT_BY_GROUP_BY_FIELD', () => {
|
||||
const { result } = renderUseChartSettingsValues(verticalBar2DConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD,
|
||||
);
|
||||
|
||||
expect(value).toBeDefined();
|
||||
expect(typeof value).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Horizontal bar chart (2D - with secondary grouping)', () => {
|
||||
const horizontalBar2DConfig = buildBarChartConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-company-name',
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-stage',
|
||||
secondaryAxisOrderBy: GraphOrderBy.FIELD_DESC,
|
||||
});
|
||||
|
||||
it('should return SAME primary field for DATA_ON_DISPLAY_X (no swapping)', () => {
|
||||
const { result } = renderUseChartSettingsValues(horizontalBar2DConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
expect(value).toBe('Company Name');
|
||||
});
|
||||
|
||||
it('should return SAME secondary field for GROUP_BY (no swapping)', () => {
|
||||
const { result } = renderUseChartSettingsValues(horizontalBar2DConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
|
||||
);
|
||||
|
||||
expect(value).toBe('Stage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Composite field with subfield', () => {
|
||||
const configWithSubField = buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-full-name',
|
||||
primaryAxisGroupBySubFieldName: 'firstName',
|
||||
});
|
||||
|
||||
it('should return field label with subfield label for DATA_ON_DISPLAY_X', () => {
|
||||
const { result } = renderUseChartSettingsValues(configWithSubField);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
expect(value).toContain('Full Name');
|
||||
expect(typeof value).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stacked bars setting', () => {
|
||||
it('should return false when groupMode is GROUPED', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
groupMode: BarChartGroupMode.GROUPED,
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.STACKED_BARS,
|
||||
);
|
||||
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when groupMode is STACKED', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
groupMode: BarChartGroupMode.STACKED,
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.STACKED_BARS,
|
||||
);
|
||||
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Colors setting', () => {
|
||||
it('should return undefined when no color is set', () => {
|
||||
const config = buildBarChartConfiguration({});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.COLORS,
|
||||
);
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return capitalized color when color is set', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
color: 'blue',
|
||||
} as Partial<BarChartConfiguration>);
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.COLORS,
|
||||
);
|
||||
|
||||
expect(value).toBe('Blue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('No configuration', () => {
|
||||
it('should return undefined function when configuration is undefined', () => {
|
||||
const { result } = renderUseChartSettingsValues(undefined as any);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
|
||||
);
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases - Missing fields', () => {
|
||||
it('should handle missing primaryAxisGroupByFieldMetadataId gracefully', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'non-existent-field-id',
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing aggregateFieldMetadataId gracefully', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
aggregateFieldMetadataId: 'non-existent-aggregate-field',
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y,
|
||||
);
|
||||
|
||||
expect(value).toBe('');
|
||||
});
|
||||
|
||||
it('should handle missing secondaryAxisGroupByFieldMetadataId gracefully', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
secondaryAxisGroupByFieldMetadataId: 'non-existent-secondary-field',
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
|
||||
);
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing objectMetadataItem gracefully', () => {
|
||||
const config = buildBarChartConfiguration({});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useChartSettingsValues({
|
||||
objectMetadataId: 'non-existent-object-id',
|
||||
configuration: config,
|
||||
}),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<RecoilRoot
|
||||
initializeState={({ set }) => {
|
||||
set(objectMetadataItemsState, [mockObjectMetadataItem]);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecoilRoot>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
|
||||
);
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases - Null/undefined orderBy', () => {
|
||||
it('should handle null primaryAxisOrderBy gracefully', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
primaryAxisOrderBy: null,
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X,
|
||||
);
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null secondaryAxisOrderBy gracefully', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-stage',
|
||||
secondaryAxisOrderBy: null,
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD,
|
||||
);
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases - Missing axisNameDisplay', () => {
|
||||
it('should handle undefined axisNameDisplay gracefully', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
axisNameDisplay: undefined as any,
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME,
|
||||
);
|
||||
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases - Composite field with missing subfield', () => {
|
||||
it('should handle composite field without subfield gracefully', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'field-full-name',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
expect(value).toBe('Full Name');
|
||||
});
|
||||
|
||||
it('should handle invalid subfield name gracefully', () => {
|
||||
const config = buildBarChartConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'field-full-name',
|
||||
primaryAxisGroupBySubFieldName: 'invalidSubField' as any,
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(config);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
expect(value).toBeDefined();
|
||||
expect(typeof value).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Critical: Proves no swapping occurs', () => {
|
||||
it('horizontal bar with NULL secondary should still return primary for DATA_ON_DISPLAY_X', () => {
|
||||
const horizontalConfig = buildBarChartConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-company-name',
|
||||
secondaryAxisGroupByFieldMetadataId: null,
|
||||
});
|
||||
|
||||
const { result } = renderUseChartSettingsValues(horizontalConfig);
|
||||
|
||||
const value = result.current.getChartSettingsValues(
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
);
|
||||
|
||||
expect(value).toBe('Company Name');
|
||||
expect(value).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return identical values for all settings regardless of orientation', () => {
|
||||
const baseConfig = {
|
||||
primaryAxisGroupByFieldMetadataId: 'field-company-name',
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-stage',
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
aggregateOperation: ExtendedAggregateOperations.SUM,
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
secondaryAxisOrderBy: GraphOrderBy.FIELD_DESC,
|
||||
axisNameDisplay: AxisNameDisplay.BOTH,
|
||||
displayDataLabel: true,
|
||||
};
|
||||
|
||||
const verticalConfig = buildBarChartConfiguration({
|
||||
...baseConfig,
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
});
|
||||
|
||||
const horizontalConfig = buildBarChartConfiguration({
|
||||
...baseConfig,
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
});
|
||||
|
||||
const { result: verticalResult } =
|
||||
renderUseChartSettingsValues(verticalConfig);
|
||||
const { result: horizontalResult } =
|
||||
renderUseChartSettingsValues(horizontalConfig);
|
||||
|
||||
// Test all setting IDs return IDENTICAL values
|
||||
const settingIds = [
|
||||
CHART_CONFIGURATION_SETTING_IDS.SOURCE,
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X,
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y,
|
||||
CHART_CONFIGURATION_SETTING_IDS.GROUP_BY,
|
||||
CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X,
|
||||
CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD,
|
||||
CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME,
|
||||
CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS,
|
||||
];
|
||||
|
||||
settingIds.forEach((settingId) => {
|
||||
const verticalValue =
|
||||
verticalResult.current.getChartSettingsValues(settingId);
|
||||
const horizontalValue =
|
||||
horizontalResult.current.getChartSettingsValues(settingId);
|
||||
|
||||
expect(verticalValue).toEqual(horizontalValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+55
-48
@@ -9,6 +9,7 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record-
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { type GraphOrderBy } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useChartSettingsValues = ({
|
||||
objectMetadataId,
|
||||
@@ -37,20 +38,43 @@ export const useChartSettingsValues = ({
|
||||
};
|
||||
}
|
||||
|
||||
const groupByFieldX =
|
||||
'groupByFieldMetadataIdX' in configuration
|
||||
? objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === configuration.groupByFieldMetadataIdX,
|
||||
)
|
||||
: undefined;
|
||||
const isBarOrLineChart =
|
||||
configuration.__typename === 'BarChartConfiguration' ||
|
||||
configuration.__typename === 'LineChartConfiguration';
|
||||
|
||||
let groupByFieldXId: string | undefined;
|
||||
let groupByFieldYId: string | undefined;
|
||||
let groupBySubFieldNameX: CompositeFieldSubFieldName | undefined;
|
||||
let groupBySubFieldNameY: CompositeFieldSubFieldName | undefined;
|
||||
let xAxisOrderBy: GraphOrderBy | undefined | null;
|
||||
let groupByOrderBy: GraphOrderBy | undefined | null;
|
||||
|
||||
if (isBarOrLineChart) {
|
||||
groupByFieldXId = configuration.primaryAxisGroupByFieldMetadataId;
|
||||
groupByFieldYId = configuration.secondaryAxisGroupByFieldMetadataId;
|
||||
groupBySubFieldNameX = configuration.primaryAxisGroupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined;
|
||||
groupBySubFieldNameY = configuration.secondaryAxisGroupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined;
|
||||
xAxisOrderBy = configuration.primaryAxisOrderBy;
|
||||
groupByOrderBy = configuration.secondaryAxisOrderBy;
|
||||
}
|
||||
|
||||
const groupByFieldX = isDefined(groupByFieldXId)
|
||||
? objectMetadataItem?.fields.find((field) => field.id === groupByFieldXId)
|
||||
: undefined;
|
||||
|
||||
const groupByFieldY = isDefined(groupByFieldYId)
|
||||
? objectMetadataItem?.fields.find((field) => field.id === groupByFieldYId)
|
||||
: undefined;
|
||||
|
||||
const groupBySubFieldNameXLabel =
|
||||
'groupBySubFieldNameX' in configuration && isDefined(groupByFieldX)
|
||||
isDefined(groupBySubFieldNameX) && isDefined(groupByFieldX)
|
||||
? getFieldLabelWithSubField({
|
||||
field: groupByFieldX,
|
||||
subFieldName:
|
||||
configuration.groupBySubFieldNameX as CompositeFieldSubFieldName,
|
||||
subFieldName: groupBySubFieldNameX,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
@@ -61,53 +85,36 @@ export const useChartSettingsValues = ({
|
||||
|
||||
const yAxisAggregateOperation = configuration.aggregateOperation;
|
||||
|
||||
const groupByFieldY =
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === configuration.groupByFieldMetadataIdY,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const xAxisOrderBy =
|
||||
'orderByX' in configuration ? configuration.orderByX : undefined;
|
||||
|
||||
const xAxisOrderByLabel =
|
||||
isDefined(xAxisOrderBy) && 'groupByFieldMetadataIdX' in configuration
|
||||
isDefined(xAxisOrderBy) && isDefined(groupByFieldXId)
|
||||
? getXSortOptionLabel({
|
||||
graphOrderBy: xAxisOrderBy,
|
||||
groupByFieldMetadataIdX: configuration.groupByFieldMetadataIdX,
|
||||
groupBySubFieldNameX:
|
||||
configuration.groupBySubFieldNameX as CompositeFieldSubFieldName,
|
||||
groupByFieldMetadataIdX: groupByFieldXId,
|
||||
groupBySubFieldNameX: groupBySubFieldNameX,
|
||||
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
|
||||
aggregateOperation: configuration.aggregateOperation ?? undefined,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const groupByOrderBy =
|
||||
'orderByY' in configuration
|
||||
? configuration.orderByY
|
||||
: 'orderBy' in configuration
|
||||
? configuration.orderBy
|
||||
: undefined;
|
||||
if (configuration.__typename === 'PieChartConfiguration') {
|
||||
groupByOrderBy = configuration.orderBy;
|
||||
groupByFieldYId = configuration.groupByFieldMetadataId;
|
||||
groupBySubFieldNameY = configuration.groupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined;
|
||||
}
|
||||
|
||||
const finalGroupByFieldYId = groupByFieldYId;
|
||||
const finalGroupBySubFieldNameY = groupBySubFieldNameY;
|
||||
|
||||
const groupByOrderByLabel =
|
||||
isDefined(groupByOrderBy) &&
|
||||
getGroupBySortOptionLabel({
|
||||
graphOrderBy: groupByOrderBy,
|
||||
groupByFieldMetadataId:
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? configuration.groupByFieldMetadataIdY
|
||||
: 'groupByFieldMetadataId' in configuration
|
||||
? configuration.groupByFieldMetadataId
|
||||
: undefined,
|
||||
groupBySubFieldName:
|
||||
'groupBySubFieldNameY' in configuration
|
||||
? (configuration.groupBySubFieldNameY as CompositeFieldSubFieldName)
|
||||
: 'groupBySubFieldName' in configuration
|
||||
? (configuration.groupBySubFieldName as CompositeFieldSubFieldName)
|
||||
: undefined,
|
||||
});
|
||||
isDefined(groupByOrderBy) && isDefined(finalGroupByFieldYId)
|
||||
? getGroupBySortOptionLabel({
|
||||
graphOrderBy: groupByOrderBy,
|
||||
groupByFieldMetadataId: finalGroupByFieldYId,
|
||||
groupBySubFieldName: finalGroupBySubFieldNameY,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const getChartSettingsValues = (
|
||||
itemId: CHART_CONFIGURATION_SETTING_IDS,
|
||||
@@ -119,7 +126,7 @@ export const useChartSettingsValues = ({
|
||||
return groupBySubFieldNameXLabel ?? groupByFieldX?.label;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.COLORS:
|
||||
return 'color' in configuration && isDefined(configuration.color)
|
||||
? capitalize(configuration.color as string)
|
||||
? capitalize(configuration.color)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y: {
|
||||
const hasAggregateLabel = isDefined(aggregateField?.label);
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
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 { 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';
|
||||
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 { 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';
|
||||
import { IconAxisX, IconAxisY } from 'twenty-ui/display';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
import { getBarChartSettings } from '../getBarChartSettings';
|
||||
|
||||
describe('getBarChartSettings', () => {
|
||||
describe('Vertical bar chart', () => {
|
||||
it('should place primary axis items under "X axis" heading', () => {
|
||||
const result = getBarChartSettings(GraphType.VERTICAL_BAR);
|
||||
|
||||
const xAxisGroup = result.find((group) => group.heading === 'X axis');
|
||||
|
||||
expect(xAxisGroup).toBeDefined();
|
||||
expect(xAxisGroup?.items).toHaveLength(2);
|
||||
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);
|
||||
expect(xAxisGroup?.items[1]).toEqual(SORT_BY_X_SETTING);
|
||||
});
|
||||
|
||||
it('should place secondary axis items under "Y axis" heading', () => {
|
||||
const result = getBarChartSettings(GraphType.VERTICAL_BAR);
|
||||
|
||||
const yAxisGroup = result.find((group) => group.heading === 'Y axis');
|
||||
|
||||
expect(yAxisGroup).toBeDefined();
|
||||
expect(yAxisGroup?.items).toHaveLength(3);
|
||||
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);
|
||||
});
|
||||
|
||||
it('should have all expected groups in correct order', () => {
|
||||
const result = getBarChartSettings(GraphType.VERTICAL_BAR);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result[0].heading).toBe('Data');
|
||||
expect(result[1].heading).toBe('X axis');
|
||||
expect(result[2].heading).toBe('Y axis');
|
||||
expect(result[3].heading).toBe('Style');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Horizontal bar chart', () => {
|
||||
it('should place SECONDARY axis items under "X axis" heading', () => {
|
||||
const result = getBarChartSettings(GraphType.HORIZONTAL_BAR);
|
||||
|
||||
const xAxisGroup = result.find((group) => group.heading === 'X axis');
|
||||
|
||||
expect(xAxisGroup).toBeDefined();
|
||||
expect(xAxisGroup?.items).toHaveLength(3);
|
||||
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);
|
||||
});
|
||||
|
||||
it('should place PRIMARY axis items under "Y axis" heading', () => {
|
||||
const result = getBarChartSettings(GraphType.HORIZONTAL_BAR);
|
||||
|
||||
const yAxisGroup = result.find((group) => group.heading === 'Y axis');
|
||||
|
||||
expect(yAxisGroup).toBeDefined();
|
||||
expect(yAxisGroup?.items).toHaveLength(2);
|
||||
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);
|
||||
expect(yAxisGroup?.items[1]).toEqual(SORT_BY_X_SETTING);
|
||||
});
|
||||
|
||||
it('should have all expected groups in correct order', () => {
|
||||
const result = getBarChartSettings(GraphType.HORIZONTAL_BAR);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result[0].heading).toBe('Data');
|
||||
expect(result[1].heading).toBe('X axis');
|
||||
expect(result[2].heading).toBe('Y axis');
|
||||
expect(result[3].heading).toBe('Style');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Common groups', () => {
|
||||
it('should have consistent Data group for both orientations', () => {
|
||||
const verticalResult = getBarChartSettings(GraphType.VERTICAL_BAR);
|
||||
const horizontalResult = getBarChartSettings(GraphType.HORIZONTAL_BAR);
|
||||
|
||||
const verticalDataGroup = verticalResult.find(
|
||||
(group) => group.heading === 'Data',
|
||||
);
|
||||
const horizontalDataGroup = horizontalResult.find(
|
||||
(group) => group.heading === 'Data',
|
||||
);
|
||||
|
||||
expect(verticalDataGroup?.items).toEqual(horizontalDataGroup?.items);
|
||||
expect(verticalDataGroup?.items).toEqual([
|
||||
CHART_DATA_SOURCE_SETTING,
|
||||
FILTER_SETTING,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have consistent Style group for both orientations', () => {
|
||||
const verticalResult = getBarChartSettings(GraphType.VERTICAL_BAR);
|
||||
const horizontalResult = getBarChartSettings(GraphType.HORIZONTAL_BAR);
|
||||
|
||||
const verticalStyleGroup = verticalResult.find(
|
||||
(group) => group.heading === 'Style',
|
||||
);
|
||||
const horizontalStyleGroup = horizontalResult.find(
|
||||
(group) => group.heading === 'Style',
|
||||
);
|
||||
|
||||
expect(verticalStyleGroup?.items).toEqual(horizontalStyleGroup?.items);
|
||||
expect(verticalStyleGroup?.items).toEqual([
|
||||
COLORS_SETTING,
|
||||
AXIS_NAME_SETTING,
|
||||
STACKED_BARS_SETTING,
|
||||
DATA_LABELS_SETTING,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+48
-27
@@ -10,31 +10,52 @@ import { SORT_BY_GROUP_BY_FIELD_SETTING } from '@/command-menu/pages/page-layout
|
||||
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';
|
||||
import { type ChartSettingsGroup } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup';
|
||||
import { IconAxisX, IconAxisY } from 'twenty-ui/display';
|
||||
import { GraphType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const BAR_CHART_SETTINGS: ChartSettingsGroup[] = [
|
||||
{
|
||||
heading: 'Data',
|
||||
items: [CHART_DATA_SOURCE_SETTING, FILTER_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'X axis',
|
||||
items: [DATA_DISPLAY_X_SETTING, SORT_BY_X_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'Y axis',
|
||||
items: [
|
||||
DATA_DISPLAY_Y_SETTING,
|
||||
GROUP_BY_SETTING,
|
||||
SORT_BY_GROUP_BY_FIELD_SETTING,
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Style',
|
||||
items: [
|
||||
COLORS_SETTING,
|
||||
AXIS_NAME_SETTING,
|
||||
STACKED_BARS_SETTING,
|
||||
DATA_LABELS_SETTING,
|
||||
],
|
||||
},
|
||||
];
|
||||
export const getBarChartSettings = (
|
||||
graphType: GraphType.VERTICAL_BAR | GraphType.HORIZONTAL_BAR,
|
||||
): ChartSettingsGroup[] => {
|
||||
const isHorizontal = graphType === GraphType.HORIZONTAL_BAR;
|
||||
|
||||
const dataDisplayXIcon = isHorizontal ? IconAxisY : IconAxisX;
|
||||
const dataDisplayYIcon = isHorizontal ? IconAxisX : IconAxisY;
|
||||
|
||||
const primaryAxisItems = [
|
||||
{ ...DATA_DISPLAY_X_SETTING, Icon: dataDisplayXIcon },
|
||||
SORT_BY_X_SETTING,
|
||||
];
|
||||
|
||||
const secondaryAxisItems = [
|
||||
{ ...DATA_DISPLAY_Y_SETTING, Icon: dataDisplayYIcon },
|
||||
GROUP_BY_SETTING,
|
||||
SORT_BY_GROUP_BY_FIELD_SETTING,
|
||||
];
|
||||
|
||||
const xAxisItems = isHorizontal ? secondaryAxisItems : primaryAxisItems;
|
||||
const yAxisItems = isHorizontal ? primaryAxisItems : secondaryAxisItems;
|
||||
|
||||
return [
|
||||
{
|
||||
heading: 'Data',
|
||||
items: [CHART_DATA_SOURCE_SETTING, FILTER_SETTING],
|
||||
},
|
||||
{
|
||||
heading: 'X axis',
|
||||
items: xAxisItems,
|
||||
},
|
||||
{
|
||||
heading: 'Y axis',
|
||||
items: yAxisItems,
|
||||
},
|
||||
{
|
||||
heading: 'Style',
|
||||
items: [
|
||||
COLORS_SETTING,
|
||||
AXIS_NAME_SETTING,
|
||||
STACKED_BARS_SETTING,
|
||||
DATA_LABELS_SETTING,
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
+7
-3
@@ -12,9 +12,13 @@ export const getFieldLabelWithSubField = ({
|
||||
field: FieldMetadataItem | undefined;
|
||||
subFieldName?: CompositeFieldSubFieldName;
|
||||
}): string => {
|
||||
const subFieldNameLabel = isDefined(subFieldName)
|
||||
? getCompositeSubFieldLabel(field?.type as CompositeFieldType, subFieldName)
|
||||
: undefined;
|
||||
const subFieldNameLabel =
|
||||
isDefined(subFieldName) && isDefined(field)
|
||||
? getCompositeSubFieldLabel(
|
||||
field.type as CompositeFieldType,
|
||||
subFieldName,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const fieldLabel =
|
||||
isDefined(subFieldNameLabel) && isDefined(field?.label)
|
||||
|
||||
Reference in New Issue
Block a user