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:
nitin
2025-10-15 11:38:48 +02:00
committed by GitHub
parent d21be90c5d
commit 436084f8f4
48 changed files with 1433 additions and 392 deletions
@@ -287,16 +287,16 @@ export type BarChartConfiguration = {
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataIdX: Scalars['UUID'];
groupByFieldMetadataIdY?: Maybe<Scalars['UUID']>;
groupBySubFieldNameX?: Maybe<Scalars['String']>;
groupBySubFieldNameY?: Maybe<Scalars['String']>;
groupMode?: Maybe<BarChartGroupMode>;
omitNullValues?: Maybe<Scalars['Boolean']>;
orderByX?: Maybe<GraphOrderBy>;
orderByY?: Maybe<GraphOrderBy>;
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
primaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
primaryAxisOrderBy?: Maybe<GraphOrderBy>;
rangeMax?: Maybe<Scalars['Float']>;
rangeMin?: Maybe<Scalars['Float']>;
secondaryAxisGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
};
/** Display mode for bar charts with secondary grouping */
@@ -1448,11 +1448,12 @@ export enum GraphOrderBy {
/** Type of graph widget */
export enum GraphType {
BAR = 'BAR',
GAUGE = 'GAUGE',
HORIZONTAL_BAR = 'HORIZONTAL_BAR',
LINE = 'LINE',
NUMBER = 'NUMBER',
PIE = 'PIE'
PIE = 'PIE',
VERTICAL_BAR = 'VERTICAL_BAR'
}
export type GridPosition = {
@@ -1632,15 +1633,15 @@ export type LineChartConfiguration = {
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataIdX: Scalars['UUID'];
groupByFieldMetadataIdY?: Maybe<Scalars['UUID']>;
groupBySubFieldNameX?: Maybe<Scalars['String']>;
groupBySubFieldNameY?: Maybe<Scalars['String']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
orderByX?: Maybe<GraphOrderBy>;
orderByY?: Maybe<GraphOrderBy>;
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
primaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
primaryAxisOrderBy?: Maybe<GraphOrderBy>;
rangeMax?: Maybe<Scalars['Float']>;
rangeMin?: Maybe<Scalars['Float']>;
secondaryAxisGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
};
export type LinkMetadata = {
+29 -28
View File
@@ -287,16 +287,16 @@ export type BarChartConfiguration = {
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataIdX: Scalars['UUID'];
groupByFieldMetadataIdY?: Maybe<Scalars['UUID']>;
groupBySubFieldNameX?: Maybe<Scalars['String']>;
groupBySubFieldNameY?: Maybe<Scalars['String']>;
groupMode?: Maybe<BarChartGroupMode>;
omitNullValues?: Maybe<Scalars['Boolean']>;
orderByX?: Maybe<GraphOrderBy>;
orderByY?: Maybe<GraphOrderBy>;
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
primaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
primaryAxisOrderBy?: Maybe<GraphOrderBy>;
rangeMax?: Maybe<Scalars['Float']>;
rangeMin?: Maybe<Scalars['Float']>;
secondaryAxisGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
};
/** Display mode for bar charts with secondary grouping */
@@ -1405,11 +1405,12 @@ export enum GraphOrderBy {
/** Type of graph widget */
export enum GraphType {
BAR = 'BAR',
GAUGE = 'GAUGE',
HORIZONTAL_BAR = 'HORIZONTAL_BAR',
LINE = 'LINE',
NUMBER = 'NUMBER',
PIE = 'PIE'
PIE = 'PIE',
VERTICAL_BAR = 'VERTICAL_BAR'
}
export type GridPosition = {
@@ -1589,15 +1590,15 @@ export type LineChartConfiguration = {
displayDataLabel?: Maybe<Scalars['Boolean']>;
filter?: Maybe<Scalars['JSON']>;
graphType: GraphType;
groupByFieldMetadataIdX: Scalars['UUID'];
groupByFieldMetadataIdY?: Maybe<Scalars['UUID']>;
groupBySubFieldNameX?: Maybe<Scalars['String']>;
groupBySubFieldNameY?: Maybe<Scalars['String']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
orderByX?: Maybe<GraphOrderBy>;
orderByY?: Maybe<GraphOrderBy>;
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
primaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
primaryAxisOrderBy?: Maybe<GraphOrderBy>;
rangeMax?: Maybe<Scalars['Float']>;
rangeMin?: Maybe<Scalars['Float']>;
secondaryAxisGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
};
export type LinkMetadata = {
@@ -4487,7 +4488,7 @@ export type SearchQueryVariables = Exact<{
export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResultConnection', edges: Array<{ __typename?: 'SearchResultEdge', cursor: string, node: { __typename?: 'SearchRecord', recordId: any, objectNameSingular: string, label: string, imageUrl?: string | null, tsRankCD: number, tsRank: number } }>, pageInfo: { __typename?: 'SearchResultPageInfo', hasNextPage: boolean, endCursor?: string | null } } };
export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, groupBySubFieldNameX?: string | null, orderByX?: GraphOrderBy | null, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, groupBySubFieldNameX?: string | null, orderByX?: GraphOrderBy | null, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel?: boolean | null, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | null };
export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel?: boolean | null, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | null };
export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
id: Scalars['String'];
@@ -4495,7 +4496,7 @@ export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
}>;
export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, groupBySubFieldNameX?: string | null, orderByX?: GraphOrderBy | null, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupByFieldMetadataIdX: any, groupBySubFieldNameX?: string | null, orderByX?: GraphOrderBy | null, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel?: boolean | null, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | null }> | null }> | null } };
export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'IframeConfiguration', url: string } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'NumberChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel?: boolean | null, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null } | null }> | null }> | null } };
export type OnDbEventSubscriptionVariables = Exact<{
input: OnDbEventInput;
@@ -4796,12 +4797,12 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
graphType
aggregateFieldMetadataId
aggregateOperation
groupByFieldMetadataIdX
groupBySubFieldNameX
orderByX
groupByFieldMetadataIdY
groupBySubFieldNameY
orderByY
primaryAxisGroupByFieldMetadataId
primaryAxisGroupBySubFieldName
primaryAxisOrderBy
secondaryAxisGroupByFieldMetadataId
secondaryAxisGroupBySubFieldName
secondaryAxisOrderBy
omitNullValues
axisNameDisplay
displayDataLabel
@@ -4816,12 +4817,12 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
graphType
aggregateFieldMetadataId
aggregateOperation
groupByFieldMetadataIdX
groupBySubFieldNameX
orderByX
groupByFieldMetadataIdY
groupBySubFieldNameY
orderByY
primaryAxisGroupByFieldMetadataId
primaryAxisGroupBySubFieldName
primaryAxisOrderBy
secondaryAxisGroupByFieldMetadataId
secondaryAxisGroupBySubFieldName
secondaryAxisOrderBy
omitNullValues
axisNameDisplay
displayDataLabel
@@ -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,
@@ -34,7 +34,7 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
if (!isDefined(pageLayoutEditingWidgetId)) {
const fieldSelection = buildBarChartFieldSelection();
const newWidget = createPageLayoutGraphWidget({
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
fieldSelection,
});
@@ -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,
},
@@ -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
@@ -9,8 +9,8 @@ export const ChartGroupByFieldSelectionDropdownContent = () => {
<ChartGroupByFieldSelectionDropdownContentBase<
BarChartConfiguration | LineChartConfiguration
>
fieldMetadataIdKey="groupByFieldMetadataIdY"
subFieldNameKey="groupBySubFieldNameY"
fieldMetadataIdKey="secondaryAxisGroupByFieldMetadataId"
subFieldNameKey="secondaryAxisGroupBySubFieldName"
/>
);
};
@@ -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={() => {
@@ -9,8 +9,8 @@ export const ChartXAxisFieldSelectionDropdownContent = () => {
<ChartGroupByFieldSelectionDropdownContentBase<
BarChartConfiguration | LineChartConfiguration
>
fieldMetadataIdKey="groupByFieldMetadataIdX"
subFieldNameKey="groupBySubFieldNameX"
fieldMetadataIdKey="primaryAxisGroupByFieldMetadataId"
subFieldNameKey="primaryAxisGroupBySubFieldName"
/>
);
};
@@ -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>
@@ -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`,
@@ -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',
@@ -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);
});
});
});
});
@@ -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);
@@ -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,
]);
});
});
});
@@ -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,
],
},
];
};
@@ -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)
@@ -162,11 +162,11 @@ const mixedGraphsPageLayoutMocks = {
},
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: nameField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: createdAtField.id,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
} satisfies BarChartConfiguration,
@@ -20,12 +20,12 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
graphType
aggregateFieldMetadataId
aggregateOperation
groupByFieldMetadataIdX
groupBySubFieldNameX
orderByX
groupByFieldMetadataIdY
groupBySubFieldNameY
orderByY
primaryAxisGroupByFieldMetadataId
primaryAxisGroupBySubFieldName
primaryAxisOrderBy
secondaryAxisGroupByFieldMetadataId
secondaryAxisGroupBySubFieldName
secondaryAxisOrderBy
omitNullValues
axisNameDisplay
displayDataLabel
@@ -40,12 +40,12 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
graphType
aggregateFieldMetadataId
aggregateOperation
groupByFieldMetadataIdX
groupBySubFieldNameX
orderByX
groupByFieldMetadataIdY
groupBySubFieldNameY
orderByY
primaryAxisGroupByFieldMetadataId
primaryAxisGroupBySubFieldName
primaryAxisOrderBy
secondaryAxisGroupByFieldMetadataId
secondaryAxisGroupBySubFieldName
secondaryAxisOrderBy
omitNullValues
axisNameDisplay
displayDataLabel
@@ -85,7 +85,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
act(() => {
result.current.createWidget.createPageLayoutGraphWidget({
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
});
});
@@ -163,7 +163,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
GraphType.NUMBER,
GraphType.GAUGE,
GraphType.PIE,
GraphType.BAR,
GraphType.VERTICAL_BAR,
];
const mockFieldSelections: Partial<
@@ -173,7 +173,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
objectMetadataId: 'test-object-id',
aggregateFieldMetadataId: 'test-aggregate-field-id',
},
[GraphType.BAR]: {
[GraphType.VERTICAL_BAR]: {
objectMetadataId: 'test-object-id',
groupByFieldMetadataIdX: 'test-groupby-field-id',
aggregateFieldMetadataId: 'test-aggregate-field-id',
@@ -234,7 +234,7 @@ describe('useCreatePageLayoutGraphWidget', () => {
expect(() => {
result.current.createWidget.createPageLayoutGraphWidget({
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
});
}).toThrow('A tab must be selected to create a new graph widget');
});
@@ -103,11 +103,11 @@ describe('usePageLayoutDraftState', () => {
type: WidgetType.GRAPH,
gridPosition: { row: 2, column: 2, rowSpan: 2, columnSpan: 2 },
configuration: {
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: 'id',
groupByFieldMetadataIdX: 'createdAt',
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: 'createdAt',
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: false,
},
objectMetadataId: null,
@@ -50,11 +50,11 @@ describe('extractFieldMetadataIdsFromWidget', () => {
const widget = createMockWidget({
configuration: {
__typename: 'BarChartConfiguration' as const,
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: 'field-1',
aggregateOperation: ExtendedAggregateOperations.COUNT,
groupByFieldMetadataIdX: 'field-2',
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: 'field-2',
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
@@ -72,12 +72,12 @@ describe('extractFieldMetadataIdsFromWidget', () => {
const widget = createMockWidget({
configuration: {
__typename: 'BarChartConfiguration' as const,
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: 'field-1',
aggregateOperation: ExtendedAggregateOperations.COUNT,
groupByFieldMetadataIdX: 'field-2',
groupByFieldMetadataIdY: 'field-3',
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: 'field-2',
secondaryAxisGroupByFieldMetadataId: 'field-3',
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
@@ -99,8 +99,8 @@ describe('extractFieldMetadataIdsFromWidget', () => {
graphType: GraphType.LINE,
aggregateFieldMetadataId: 'field-1',
aggregateOperation: ExtendedAggregateOperations.SUM,
groupByFieldMetadataIdX: 'field-2',
orderByX: GraphOrderBy.FIELD_DESC,
primaryAxisGroupByFieldMetadataId: 'field-2',
primaryAxisOrderBy: GraphOrderBy.FIELD_DESC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
@@ -175,12 +175,12 @@ describe('extractFieldMetadataIdsFromWidget', () => {
const widget = createMockWidget({
configuration: {
__typename: 'BarChartConfiguration' as const,
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: 'field-1',
aggregateOperation: ExtendedAggregateOperations.COUNT,
groupByFieldMetadataIdX: 'field-2',
groupByFieldMetadataIdY: undefined,
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: 'field-2',
secondaryAxisGroupByFieldMetadataId: undefined,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
@@ -35,7 +35,7 @@ const createDefaultGraphConfiguration = (
case GraphType.PIE:
return null;
case GraphType.BAR:
case GraphType.VERTICAL_BAR:
if (
!isDefined(fieldSelection?.aggregateFieldMetadataId) ||
!isDefined(fieldSelection?.groupByFieldMetadataIdX)
@@ -44,13 +44,34 @@ const createDefaultGraphConfiguration = (
}
return {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
displayDataLabel: false,
color: 'blue' satisfies ThemeColor,
groupByFieldMetadataIdX: fieldSelection.groupByFieldMetadataIdX,
primaryAxisGroupByFieldMetadataId:
fieldSelection.groupByFieldMetadataIdX,
aggregateFieldMetadataId: fieldSelection.aggregateFieldMetadataId,
aggregateOperation: ExtendedAggregateOperations.SUM,
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
};
case GraphType.HORIZONTAL_BAR:
if (
!isDefined(fieldSelection?.aggregateFieldMetadataId) ||
!isDefined(fieldSelection?.groupByFieldMetadataIdX)
) {
return null;
}
return {
__typename: 'BarChartConfiguration',
graphType: GraphType.HORIZONTAL_BAR,
displayDataLabel: false,
color: 'blue' satisfies ThemeColor,
primaryAxisGroupByFieldMetadataId:
fieldSelection.groupByFieldMetadataIdX,
aggregateFieldMetadataId: fieldSelection.aggregateFieldMetadataId,
aggregateOperation: ExtendedAggregateOperations.SUM,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
};
@@ -15,11 +15,17 @@ export const extractFieldMetadataIdsFromWidget = (
switch (config.__typename) {
case 'BarChartConfiguration':
return [
config.aggregateFieldMetadataId,
config.primaryAxisGroupByFieldMetadataId,
config.secondaryAxisGroupByFieldMetadataId,
].filter(isDefined);
case 'LineChartConfiguration':
return [
config.aggregateFieldMetadataId,
config.groupByFieldMetadataIdX,
config.groupByFieldMetadataIdY,
config.primaryAxisGroupByFieldMetadataId,
config.secondaryAxisGroupByFieldMetadataId,
].filter(isDefined);
case 'PieChartConfiguration':
@@ -26,7 +26,7 @@ export const getDefaultWidgetData = (graphType: GraphType) => {
],
};
case GraphType.BAR:
case GraphType.VERTICAL_BAR:
return {
items: [
{ category: 'Jan', value: 45 },
@@ -41,6 +41,21 @@ export const getDefaultWidgetData = (graphType: GraphType) => {
layout: 'vertical' as const,
};
case GraphType.HORIZONTAL_BAR:
return {
items: [
{ category: 'Jan', value: 45 },
{ category: 'Feb', value: 52 },
{ category: 'Mar', value: 48 },
{ category: 'Apr', value: 61 },
{ category: 'May', value: 55 },
],
indexBy: 'category',
keys: ['value'],
seriesLabels: { value: 'Value' },
layout: 'horizontal' as const,
};
case GraphType.LINE:
return {
series: [
@@ -119,7 +134,8 @@ export const getWidgetTitle = (graphType: GraphType, index: number): string => {
[GraphType.NUMBER]: 'Number',
[GraphType.GAUGE]: 'Gauge',
[GraphType.PIE]: 'Pie Chart',
[GraphType.BAR]: 'Bar Chart',
[GraphType.VERTICAL_BAR]: 'Vertical Bar Chart',
[GraphType.HORIZONTAL_BAR]: 'Horizontal Bar Chart',
[GraphType.LINE]: 'Line Chart',
};
@@ -134,7 +150,8 @@ export const getWidgetSize = (graphType: GraphType) => {
return { w: 3, h: 3 };
case GraphType.PIE:
return { w: 4, h: 4 };
case GraphType.BAR:
case GraphType.VERTICAL_BAR:
case GraphType.HORIZONTAL_BAR:
return { w: 6, h: 6 };
case GraphType.LINE:
return { w: 6, h: 10 };
@@ -240,11 +240,11 @@ export const WithBarChart: Story = {
},
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: createdAtField.id,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
@@ -320,11 +320,11 @@ export const MediumWidget: Story = {
},
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: createdAtField.id,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
@@ -365,11 +365,11 @@ export const LargeWidget: Story = {
},
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: createdAtField.id,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
@@ -452,11 +452,11 @@ export const TallWidget: Story = {
},
configuration: {
__typename: 'BarChartConfiguration',
graphType: GraphType.BAR,
graphType: GraphType.VERTICAL_BAR,
aggregateOperation: ExtendedAggregateOperations.COUNT,
aggregateFieldMetadataId: idField.id,
groupByFieldMetadataIdX: createdAtField.id,
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: createdAtField.id,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
},
@@ -100,7 +100,8 @@ export const GraphWidget = ({
</Suspense>
);
case GraphType.BAR:
case GraphType.VERTICAL_BAR:
case GraphType.HORIZONTAL_BAR:
return <GraphWidgetBarChartRenderer widget={widget} />;
case GraphType.LINE:
@@ -28,6 +28,7 @@ export const GraphWidgetBarChartRenderer = ({
xAxisLabel,
yAxisLabel,
showDataLabels,
layout,
loading,
error,
} = useGraphBarChartWidgetData({
@@ -57,6 +58,7 @@ export const GraphWidgetBarChartRenderer = ({
xAxisLabel={xAxisLabel}
yAxisLabel={yAxisLabel}
showValues={showDataLabels}
layout={layout}
groupMode={groupMode}
id={widget.id}
displayType="shortNumber"
@@ -19,6 +19,7 @@ type UseGraphBarChartWidgetDataResult = {
xAxisLabel?: string;
yAxisLabel?: string;
showDataLabels: boolean;
layout?: 'vertical' | 'horizontal';
loading: boolean;
error?: Error;
};
@@ -1,9 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`generateGroupByQueryVariablesFromBarChartConfiguration should generate variables with composite field 1`] = `
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Horizontal bar configuration should generate variables with secondary axis 1`] = `
{
"groupBy": [
{
"stage": true,
},
{
"ownerId": true,
},
],
}
`;
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Horizontal bar configuration should generate variables with single groupBy field 1`] = `
{
"groupBy": [
{
"stage": true,
},
],
}
`;
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Vertical bar configuration should generate variables with composite field 1`] = `
{
"groupBy": [
{
"name": {
"firstName": true,
},
@@ -12,7 +35,7 @@ exports[`generateGroupByQueryVariablesFromBarChartConfiguration should generate
}
`;
exports[`generateGroupByQueryVariablesFromBarChartConfiguration should generate variables with single groupBy field 1`] = `
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Vertical bar configuration should generate variables with single groupBy field 1`] = `
{
"groupBy": [
{
@@ -22,7 +45,7 @@ exports[`generateGroupByQueryVariablesFromBarChartConfiguration should generate
}
`;
exports[`generateGroupByQueryVariablesFromBarChartConfiguration should generate variables with two groupBy fields 1`] = `
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Vertical bar configuration should generate variables with two groupBy fields 1`] = `
{
"groupBy": [
{
@@ -1,7 +1,9 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import {
type BarChartConfiguration,
ExtendedAggregateOperations,
FieldMetadataType,
GraphType,
} from '~/generated-metadata/graphql';
import { generateGroupByQueryVariablesFromBarChartConfiguration } from '../generateGroupByQueryVariablesFromBarChartConfiguration';
@@ -34,52 +36,96 @@ describe('generateGroupByQueryVariablesFromBarChartConfiguration', () => {
],
} as ObjectMetadataItem;
it('should generate variables with single groupBy field', () => {
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: {
groupByFieldMetadataIdX: 'field-1',
groupBySubFieldNameX: null,
} as BarChartConfiguration,
const buildConfiguration = (
overrides: Partial<BarChartConfiguration>,
): BarChartConfiguration =>
({
__typename: 'BarChartConfiguration',
aggregateFieldMetadataId: 'aggregate-field',
aggregateOperation: ExtendedAggregateOperations.COUNT,
graphType: GraphType.VERTICAL_BAR,
primaryAxisGroupByFieldMetadataId: 'field-1',
...overrides,
}) as BarChartConfiguration;
describe('Vertical bar configuration', () => {
it('should generate variables with single groupBy field', () => {
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: buildConfiguration({
graphType: GraphType.VERTICAL_BAR,
primaryAxisGroupByFieldMetadataId: 'field-1',
primaryAxisGroupBySubFieldName: null,
}),
});
expect(result).toMatchSnapshot();
});
expect(result).toMatchSnapshot();
});
it('should generate variables with two groupBy fields', () => {
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: buildConfiguration({
graphType: GraphType.VERTICAL_BAR,
primaryAxisGroupByFieldMetadataId: 'field-1',
primaryAxisGroupBySubFieldName: null,
secondaryAxisGroupByFieldMetadataId: 'field-2',
secondaryAxisGroupBySubFieldName: null,
}),
});
it('should generate variables with two groupBy fields', () => {
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: {
groupByFieldMetadataIdX: 'field-1',
groupBySubFieldNameX: null,
groupByFieldMetadataIdY: 'field-2',
groupBySubFieldNameY: null,
} as BarChartConfiguration,
expect(result).toMatchSnapshot();
});
expect(result).toMatchSnapshot();
it('should generate variables with composite field', () => {
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: buildConfiguration({
graphType: GraphType.VERTICAL_BAR,
primaryAxisGroupByFieldMetadataId: 'field-4',
primaryAxisGroupBySubFieldName: 'firstName',
}),
});
expect(result).toMatchSnapshot();
});
});
it('should generate variables with composite field', () => {
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: {
groupByFieldMetadataIdX: 'field-4',
groupBySubFieldNameX: 'firstName',
} as BarChartConfiguration,
describe('Horizontal bar configuration', () => {
it('should generate variables with single groupBy field', () => {
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: buildConfiguration({
graphType: GraphType.HORIZONTAL_BAR,
primaryAxisGroupByFieldMetadataId: 'field-1',
primaryAxisGroupBySubFieldName: null,
}),
});
expect(result).toMatchSnapshot();
});
expect(result).toMatchSnapshot();
it('should generate variables with secondary axis', () => {
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: buildConfiguration({
graphType: GraphType.HORIZONTAL_BAR,
primaryAxisGroupByFieldMetadataId: 'field-1',
secondaryAxisGroupByFieldMetadataId: 'field-2',
}),
});
expect(result).toMatchSnapshot();
});
});
it('should throw error when groupBy field X not found', () => {
it('should throw error when primary axis field not found', () => {
expect(() =>
generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem: mockObjectMetadataItem,
barChartConfiguration: {
groupByFieldMetadataIdX: 'invalid-field',
groupBySubFieldNameX: null,
} as BarChartConfiguration,
barChartConfiguration: buildConfiguration({
primaryAxisGroupByFieldMetadataId: 'invalid-field',
}),
}),
).toThrow('Field with id invalid-field not found in object metadata');
});
@@ -27,20 +27,22 @@ export const areChartConfigurationFieldsValidForQuery = (
switch (configuration.__typename) {
case 'BarChartConfiguration':
case 'LineChartConfiguration': {
const hasRequiredXFields =
case 'LineChartConfiguration':
return (
fieldExists(
configuration.aggregateFieldMetadataId,
objectMetadataItem,
) &&
fieldExists(configuration.groupByFieldMetadataIdX, objectMetadataItem);
const hasValidYField =
!isDefined(configuration.groupByFieldMetadataIdY) ||
fieldExists(configuration.groupByFieldMetadataIdY, objectMetadataItem);
return hasRequiredXFields && hasValidYField;
}
fieldExists(
configuration.primaryAxisGroupByFieldMetadataId,
objectMetadataItem,
) &&
(!isDefined(configuration.secondaryAxisGroupByFieldMetadataId) ||
fieldExists(
configuration.secondaryAxisGroupByFieldMetadataId,
objectMetadataItem,
))
);
case 'PieChartConfiguration':
return (
@@ -25,17 +25,29 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
objectMetadataItem: ObjectMetadataItem;
barChartConfiguration: BarChartConfiguration;
}) => {
const groupByFieldXId =
barChartConfiguration.primaryAxisGroupByFieldMetadataId;
const groupByFieldYId =
barChartConfiguration.secondaryAxisGroupByFieldMetadataId;
const groupBySubFieldNameX =
barChartConfiguration.primaryAxisGroupBySubFieldName ?? undefined;
const groupBySubFieldNameY =
barChartConfiguration.secondaryAxisGroupBySubFieldName ?? undefined;
const groupByFieldX = objectMetadataItem.fields.find(
(field) => field.id === barChartConfiguration.groupByFieldMetadataIdX,
(field) => field.id === groupByFieldXId,
);
const groupByFieldY = objectMetadataItem.fields.find(
(field) => field.id === barChartConfiguration.groupByFieldMetadataIdY,
);
const groupByFieldY = isDefined(groupByFieldYId)
? objectMetadataItem.fields.find((field) => field.id === groupByFieldYId)
: undefined;
if (!isDefined(groupByFieldX)) {
if (!isDefined(groupByFieldX) || !isDefined(groupByFieldXId)) {
throw new Error(
`Field with id ${barChartConfiguration.groupByFieldMetadataIdX} not found in object metadata`,
`Field with id ${groupByFieldXId} not found in object metadata`,
);
}
@@ -46,7 +58,7 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
groupBy.push(
buildGroupByFieldObject({
field: groupByFieldX,
subFieldName: barChartConfiguration.groupBySubFieldNameX,
subFieldName: groupBySubFieldNameX,
}),
);
@@ -54,7 +66,7 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
groupBy.push(
buildGroupByFieldObject({
field: groupByFieldY,
subFieldName: barChartConfiguration.groupBySubFieldNameY,
subFieldName: groupBySubFieldNameY,
}),
);
}
@@ -62,18 +74,21 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
const orderBy: Array<Record<string, string>> = [];
// TODO: Add orderBy back in when the backend is ready
// if (isDefined(barChartConfiguration.orderByX)) {
// if (isDefined(barChartConfiguration.primaryAxisOrderBy)) {
// orderBy.push({
// [groupByFieldX.name]: mapOrderByToDirection(
// barChartConfiguration.orderByX,
// barChartConfiguration.primaryAxisOrderBy!,
// ),
// });
// }
// if (isDefined(groupByFieldY) && isDefined(barChartConfiguration.orderByY)) {
// if (
// isDefined(groupByFieldY) &&
// isDefined(barChartConfiguration.secondaryAxisOrderBy)
// ) {
// orderBy.push({
// [groupByFieldY.name]: mapOrderByToDirection(
// barChartConfiguration.orderByY,
// barChartConfiguration.secondaryAxisOrderBy!,
// ),
// });
// }
@@ -9,6 +9,7 @@ import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
import { transformOneDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData';
import { transformTwoDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData';
import { isDefined } from 'twenty-shared/utils';
import { GraphType } from '~/generated-metadata/graphql';
import {
AxisNameDisplay,
type BarChartConfiguration,
@@ -29,6 +30,7 @@ type TransformGroupByDataToBarChartDataResult = {
xAxisLabel?: string;
yAxisLabel?: string;
showDataLabels: boolean;
layout?: 'vertical' | 'horizontal';
};
const EMPTY_BAR_CHART_RESULT: TransformGroupByDataToBarChartDataResult = {
@@ -39,6 +41,7 @@ const EMPTY_BAR_CHART_RESULT: TransformGroupByDataToBarChartDataResult = {
xAxisLabel: undefined,
yAxisLabel: undefined,
showDataLabels: false,
layout: 'vertical',
};
export const transformGroupByDataToBarChartData = ({
@@ -53,13 +56,17 @@ export const transformGroupByDataToBarChartData = ({
const groupByFieldX = objectMetadataItem.fields.find(
(field: FieldMetadataItem) =>
field.id === configuration.groupByFieldMetadataIdX,
field.id === configuration.primaryAxisGroupByFieldMetadataId,
);
const groupByFieldY = objectMetadataItem.fields.find(
(field: FieldMetadataItem) =>
field.id === configuration.groupByFieldMetadataIdY,
);
const groupByFieldY = isDefined(
configuration.secondaryAxisGroupByFieldMetadataId,
)
? objectMetadataItem.fields.find(
(field: FieldMetadataItem) =>
field.id === configuration.secondaryAxisGroupByFieldMetadataId,
)
: undefined;
const aggregateField = objectMetadataItem.fields.find(
(field: FieldMetadataItem) =>
@@ -67,12 +74,21 @@ export const transformGroupByDataToBarChartData = ({
);
if (!isDefined(groupByFieldX) || !isDefined(aggregateField)) {
return EMPTY_BAR_CHART_RESULT;
return {
...EMPTY_BAR_CHART_RESULT,
layout:
configuration.graphType === GraphType.HORIZONTAL_BAR
? 'horizontal'
: 'vertical',
};
}
const primaryAxisSubFieldName =
configuration.primaryAxisGroupBySubFieldName ?? undefined;
const indexByKey = getFieldKey({
field: groupByFieldX,
subFieldName: configuration.groupBySubFieldNameX,
subFieldName: primaryAxisSubFieldName,
});
const queryName = getGroupByQueryName(objectMetadataItem);
@@ -82,6 +98,10 @@ export const transformGroupByDataToBarChartData = ({
return {
...EMPTY_BAR_CHART_RESULT,
indexBy: indexByKey,
layout:
configuration.graphType === GraphType.HORIZONTAL_BAR
? 'horizontal'
: 'vertical',
};
}
@@ -110,6 +130,7 @@ export const transformGroupByDataToBarChartData = ({
configuration,
aggregateOperation,
objectMetadataItem,
primaryAxisSubFieldName,
})
: transformOneDimensionalGroupByToBarChartData({
rawResults,
@@ -118,12 +139,19 @@ export const transformGroupByDataToBarChartData = ({
configuration,
aggregateOperation,
objectMetadataItem,
primaryAxisSubFieldName,
});
const layout =
configuration.graphType === GraphType.HORIZONTAL_BAR
? 'horizontal'
: 'vertical';
return {
...baseResult,
xAxisLabel,
yAxisLabel,
showDataLabels,
layout,
};
};
@@ -20,6 +20,7 @@ type TransformOneDimensionalGroupByToBarChartDataParams = {
configuration: BarChartConfiguration;
aggregateOperation: string;
objectMetadataItem: ObjectMetadataItem;
primaryAxisSubFieldName?: string | null;
};
type TransformOneDimensionalGroupByToBarChartDataResult = {
@@ -36,10 +37,11 @@ export const transformOneDimensionalGroupByToBarChartData = ({
configuration,
aggregateOperation,
objectMetadataItem,
primaryAxisSubFieldName,
}: TransformOneDimensionalGroupByToBarChartDataParams): TransformOneDimensionalGroupByToBarChartDataResult => {
const indexByKey = getFieldKey({
field: groupByFieldX,
subFieldName: configuration.groupBySubFieldNameX,
subFieldName: primaryAxisSubFieldName ?? undefined,
});
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
@@ -52,7 +54,8 @@ export const transformOneDimensionalGroupByToBarChartData = ({
? formatDimensionValue({
value: dimensionValues[0],
fieldMetadata: groupByFieldX,
subFieldName: configuration.groupBySubFieldNameX ?? undefined,
subFieldName:
configuration.primaryAxisGroupBySubFieldName ?? undefined,
})
: '';
@@ -19,6 +19,7 @@ type TransformTwoDimensionalGroupByToBarChartDataParams = {
configuration: BarChartConfiguration;
aggregateOperation: string;
objectMetadataItem: ObjectMetadataItem;
primaryAxisSubFieldName?: string | null;
};
type TransformTwoDimensionalGroupByToBarChartDataResult = {
@@ -36,10 +37,11 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
configuration,
aggregateOperation,
objectMetadataItem,
primaryAxisSubFieldName,
}: TransformTwoDimensionalGroupByToBarChartDataParams): TransformTwoDimensionalGroupByToBarChartDataResult => {
const indexByKey = getFieldKey({
field: groupByFieldX,
subFieldName: configuration.groupBySubFieldNameX,
subFieldName: primaryAxisSubFieldName ?? undefined,
});
const dataMap = new Map<string, BarChartDataItem>();
@@ -53,12 +55,12 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
const xValue = formatDimensionValue({
value: dimensionValues[0],
fieldMetadata: groupByFieldX,
subFieldName: configuration.groupBySubFieldNameX ?? undefined,
subFieldName: configuration.primaryAxisGroupBySubFieldName ?? undefined,
});
const yValue = formatDimensionValue({
value: dimensionValues[1],
fieldMetadata: groupByFieldY,
subFieldName: configuration.groupBySubFieldNameY ?? undefined,
subFieldName: configuration.secondaryAxisGroupBySubFieldName ?? undefined,
});
// TODO: Add a limit to the query instead of checking here (issue: twentyhq/core-team-issues#1600)
@@ -3,6 +3,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsIn,
IsNotEmpty,
IsNumber,
IsObject,
@@ -24,9 +25,9 @@ import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.
@ObjectType('BarChartConfiguration')
export class BarChartConfigurationDTO {
@Field(() => GraphType)
@IsEnum(GraphType)
@IsIn([GraphType.VERTICAL_BAR, GraphType.HORIZONTAL_BAR])
@IsNotEmpty()
graphType: GraphType.BAR;
graphType: GraphType.VERTICAL_BAR | GraphType.HORIZONTAL_BAR;
@Field(() => UUIDScalarType)
@IsUUID()
@@ -41,32 +42,32 @@ export class BarChartConfigurationDTO {
@Field(() => UUIDScalarType)
@IsUUID()
@IsNotEmpty()
groupByFieldMetadataIdX: string;
primaryAxisGroupByFieldMetadataId: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
groupBySubFieldNameX?: string;
primaryAxisGroupBySubFieldName?: string;
@Field(() => GraphOrderBy, { nullable: true })
@IsEnum(GraphOrderBy)
@IsOptional()
orderByX?: GraphOrderBy;
primaryAxisOrderBy?: GraphOrderBy;
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@IsOptional()
groupByFieldMetadataIdY?: string;
secondaryAxisGroupByFieldMetadataId?: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
groupBySubFieldNameY?: string;
secondaryAxisGroupBySubFieldName?: string;
@Field(() => GraphOrderBy, { nullable: true })
@IsEnum(GraphOrderBy)
@IsOptional()
orderByY?: GraphOrderBy;
secondaryAxisOrderBy?: GraphOrderBy;
@Field(() => Boolean, { nullable: true })
@IsBoolean()
@@ -40,12 +40,12 @@ export class LineChartConfigurationDTO {
@Field(() => UUIDScalarType)
@IsUUID()
@IsNotEmpty()
groupByFieldMetadataIdX: string;
primaryAxisGroupByFieldMetadataId: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
groupBySubFieldNameX?: string;
primaryAxisGroupBySubFieldName?: string;
@Field(() => GraphOrderBy, {
nullable: true,
@@ -53,22 +53,22 @@ export class LineChartConfigurationDTO {
})
@IsEnum(GraphOrderBy)
@IsOptional()
orderByX?: GraphOrderBy;
primaryAxisOrderBy?: GraphOrderBy;
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@IsOptional()
groupByFieldMetadataIdY?: string;
secondaryAxisGroupByFieldMetadataId?: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
groupBySubFieldNameY?: string;
secondaryAxisGroupBySubFieldName?: string;
@Field(() => GraphOrderBy, { nullable: true })
@IsEnum(GraphOrderBy)
@IsOptional()
orderByY?: GraphOrderBy;
secondaryAxisOrderBy?: GraphOrderBy;
@Field(() => Boolean, { nullable: true })
@IsBoolean()
@@ -30,7 +30,8 @@ export const WidgetConfiguration = createUnionType({
configuration.configurationType === WidgetConfigurationType.CHART_CONFIG
) {
switch (configuration.graphType) {
case GraphType.BAR:
case GraphType.VERTICAL_BAR:
case GraphType.HORIZONTAL_BAR:
return BarChartConfigurationDTO;
case GraphType.LINE:
return LineChartConfigurationDTO;
@@ -4,7 +4,8 @@ export enum GraphType {
NUMBER = 'NUMBER',
GAUGE = 'GAUGE',
PIE = 'PIE',
BAR = 'BAR',
VERTICAL_BAR = 'VERTICAL_BAR',
HORIZONTAL_BAR = 'HORIZONTAL_BAR',
LINE = 'LINE',
}
@@ -1,12 +1,15 @@
import {
INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_IFRAME_CONFIG_BAD_URL,
INVALID_IFRAME_CONFIG_EMPTY_URL,
INVALID_IFRAME_CONFIG_MISSING_URL,
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
TEST_BAR_CHART_CONFIG,
TEST_BAR_CHART_CONFIG_MINIMAL,
TEST_VERTICAL_BAR_CHART_CONFIG,
TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
TEST_HORIZONTAL_BAR_CHART_CONFIG,
TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
TEST_GAUGE_CHART_CONFIG,
TEST_GAUGE_CHART_CONFIG_MINIMAL,
TEST_IFRAME_CONFIG,
@@ -99,32 +102,61 @@ describe('validateAndTransformWidgetConfiguration', () => {
});
});
describe('BAR graph', () => {
it('should validate full bar graph configuration', () => {
describe('VERTICAL_BAR graph', () => {
it('should validate full vertical bar graph configuration', () => {
const result = validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
TEST_BAR_CHART_CONFIG,
TEST_VERTICAL_BAR_CHART_CONFIG,
);
expect(result).toMatchObject(TEST_BAR_CHART_CONFIG);
expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG);
});
it('should validate minimal bar graph configuration', () => {
it('should validate minimal vertical bar graph configuration', () => {
const result = validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
TEST_BAR_CHART_CONFIG_MINIMAL,
TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
);
expect(result).toMatchObject(TEST_BAR_CHART_CONFIG_MINIMAL);
expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL);
});
it('should throw error for partial bar graph configuration with missing required fields', () => {
it('should throw error for partial vertical bar graph configuration with missing required fields', () => {
expect(() =>
validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
),
).toThrow(/groupByFieldMetadataIdX/);
).toThrow(/primaryAxisGroupByFieldMetadataId/);
});
});
describe('HORIZONTAL_BAR graph', () => {
it('should validate full horizontal bar graph configuration', () => {
const result = validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
TEST_HORIZONTAL_BAR_CHART_CONFIG,
);
expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG);
});
it('should validate minimal horizontal bar graph configuration', () => {
const result = validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
);
expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL);
});
it('should throw error for partial horizontal bar graph configuration with missing required fields', () => {
expect(() =>
validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
),
).toThrow(/primaryAxisGroupByFieldMetadataId/);
});
});
@@ -33,7 +33,8 @@ const validateGraphConfiguration = (
}
switch (graphType) {
case GraphType.BAR: {
case GraphType.VERTICAL_BAR:
case GraphType.HORIZONTAL_BAR: {
const instance = plainToInstance(BarChartConfigurationDTO, configuration);
const errors = validateSync(instance, {
@@ -2,6 +2,7 @@ import { isDefined } from 'twenty-shared/utils';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AxisNameDisplay } from 'src/engine/core-modules/page-layout/enums/axis-name-display.enum';
import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum';
import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PAGE_LAYOUT_TAB_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant';
@@ -142,8 +143,8 @@ export const getPageLayoutWidgetDataSeeds = (
graphType: 'LINE',
aggregateFieldMetadataId: opportunityAmountFieldId,
aggregateOperation: AggregateOperations.SUM,
groupByFieldMetadataIdX: opportunityCloseDateFieldId,
orderByX: 'FIELD_ASC',
primaryAxisGroupByFieldMetadataId: opportunityCloseDateFieldId,
primaryAxisOrderBy: 'FIELD_ASC',
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: false,
}
@@ -167,12 +168,12 @@ export const getPageLayoutWidgetDataSeeds = (
isDefined(opportunityCloseDateFieldId) &&
isDefined(opportunityStageFieldId)
? {
graphType: 'BAR',
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: opportunityAmountFieldId,
aggregateOperation: AggregateOperations.SUM,
groupByFieldMetadataIdX: opportunityCloseDateFieldId,
groupByFieldMetadataIdY: opportunityStageFieldId,
orderByX: 'FIELD_ASC',
primaryAxisGroupByFieldMetadataId: opportunityCloseDateFieldId,
secondaryAxisGroupByFieldMetadataId: opportunityStageFieldId,
primaryAxisOrderBy: 'FIELD_ASC',
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
}
@@ -196,11 +197,11 @@ export const getPageLayoutWidgetDataSeeds = (
configuration:
isDefined(rocketIdFieldId) && isDefined(rocketCreatedAtFieldId)
? {
graphType: 'BAR',
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: rocketIdFieldId,
aggregateOperation: AggregateOperations.COUNT,
groupByFieldMetadataIdX: rocketCreatedAtFieldId,
orderByX: 'FIELD_ASC',
primaryAxisGroupByFieldMetadataId: rocketCreatedAtFieldId,
primaryAxisOrderBy: 'FIELD_ASC',
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: false,
}
@@ -271,8 +272,8 @@ export const getPageLayoutWidgetDataSeeds = (
graphType: 'LINE',
aggregateFieldMetadataId: companyIdFieldId,
aggregateOperation: AggregateOperations.COUNT,
groupByFieldMetadataIdX: companyCreatedAtFieldId,
orderByX: 'FIELD_ASC',
primaryAxisGroupByFieldMetadataId: companyCreatedAtFieldId,
primaryAxisOrderBy: 'FIELD_ASC',
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: false,
}
@@ -296,13 +297,13 @@ export const getPageLayoutWidgetDataSeeds = (
isDefined(companyEmployeesFieldId) &&
isDefined(companyAddressFieldId)
? {
graphType: 'BAR',
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: companyIdFieldId,
aggregateOperation: AggregateOperations.COUNT,
groupByFieldMetadataIdX: companyEmployeesFieldId,
groupByFieldMetadataIdY: companyAddressFieldId,
groupBySubFieldNameY: 'addressCity',
orderByX: 'FIELD_ASC',
primaryAxisGroupByFieldMetadataId: companyEmployeesFieldId,
secondaryAxisGroupByFieldMetadataId: companyAddressFieldId,
secondaryAxisGroupBySubFieldName: 'addressCity',
primaryAxisOrderBy: 'FIELD_ASC',
axisNameDisplay: AxisNameDisplay.BOTH,
displayDataLabel: false,
}
@@ -463,11 +464,11 @@ export const getPageLayoutWidgetDataSeeds = (
configuration:
isDefined(personIdFieldId) && isDefined(personCityFieldId)
? {
graphType: 'BAR',
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: personIdFieldId,
aggregateOperation: AggregateOperations.COUNT,
groupByFieldMetadataIdX: personCityFieldId,
orderByX: 'VALUE_DESC',
primaryAxisGroupByFieldMetadataId: personCityFieldId,
primaryAxisOrderBy: 'VALUE_DESC',
axisNameDisplay: AxisNameDisplay.NONE,
displayDataLabel: false,
}
@@ -26,10 +26,12 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = `
graphType
aggregateFieldMetadataId
aggregateOperation
groupByFieldMetadataIdX
orderByX
groupByFieldMetadataIdY
orderByY
primaryAxisGroupByFieldMetadataId
primaryAxisGroupBySubFieldName
primaryAxisOrderBy
secondaryAxisGroupByFieldMetadataId
secondaryAxisGroupBySubFieldName
secondaryAxisOrderBy
omitNullValues
axisNameDisplay
displayDataLabel
@@ -43,10 +45,12 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = `
graphType
aggregateFieldMetadataId
aggregateOperation
groupByFieldMetadataIdX
orderByX
groupByFieldMetadataIdY
orderByY
primaryAxisGroupByFieldMetadataId
primaryAxisGroupBySubFieldName
primaryAxisOrderBy
secondaryAxisGroupByFieldMetadataId
secondaryAxisGroupBySubFieldName
secondaryAxisOrderBy
omitNullValues
axisNameDisplay
displayDataLabel
@@ -33,12 +33,12 @@ export const TEST_NUMBER_CHART_CONFIG_MINIMAL = {
displayDataLabel: false,
};
export const TEST_BAR_CHART_CONFIG = {
graphType: GraphType.BAR,
export const TEST_VERTICAL_BAR_CHART_CONFIG = {
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.SUM,
groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2,
orderByX: GraphOrderBy.FIELD_ASC,
primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: true,
axisNameDisplay: AxisNameDisplay.BOTH,
color: 'red',
@@ -48,12 +48,37 @@ export const TEST_BAR_CHART_CONFIG = {
rangeMax: 100000,
};
export const TEST_BAR_CHART_CONFIG_MINIMAL = {
graphType: GraphType.BAR,
export const TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL = {
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.COUNT,
groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2,
orderByX: GraphOrderBy.VALUE_DESC,
primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
displayDataLabel: false,
axisNameDisplay: AxisNameDisplay.NONE,
};
export const TEST_HORIZONTAL_BAR_CHART_CONFIG = {
graphType: GraphType.HORIZONTAL_BAR,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.SUM,
primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
displayDataLabel: true,
axisNameDisplay: AxisNameDisplay.BOTH,
color: 'blue',
description: 'Horizontal revenue breakdown',
omitNullValues: true,
rangeMin: 0,
rangeMax: 100000,
};
export const TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL = {
graphType: GraphType.HORIZONTAL_BAR,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.COUNT,
primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
displayDataLabel: false,
axisNameDisplay: AxisNameDisplay.NONE,
};
@@ -62,10 +87,10 @@ export const TEST_LINE_CHART_CONFIG = {
graphType: GraphType.LINE,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.AVG,
groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2,
orderByX: GraphOrderBy.FIELD_ASC,
groupByFieldMetadataIdY: TEST_FIELD_METADATA_ID_3,
orderByY: GraphOrderBy.FIELD_DESC,
primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
secondaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_3,
secondaryAxisOrderBy: GraphOrderBy.FIELD_DESC,
displayDataLabel: true,
axisNameDisplay: AxisNameDisplay.BOTH,
color: 'cyan',
@@ -79,8 +104,8 @@ export const TEST_LINE_CHART_CONFIG_MINIMAL = {
graphType: GraphType.LINE,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.MAX,
groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2,
orderByX: GraphOrderBy.VALUE_ASC,
primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
primaryAxisOrderBy: GraphOrderBy.VALUE_ASC,
displayDataLabel: false,
axisNameDisplay: AxisNameDisplay.NONE,
};
@@ -164,18 +189,32 @@ export const INVALID_NUMBER_CHART_CONFIG_INVALID_OPERATION = {
aggregateOperation: 'INVALID_OP' as any,
};
export const INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY = {
graphType: GraphType.BAR,
export const INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY = {
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.SUM,
};
export const INVALID_BAR_CHART_CONFIG_BAD_ORDER_BY = {
graphType: GraphType.BAR,
export const INVALID_VERTICAL_BAR_CHART_CONFIG_BAD_ORDER_BY = {
graphType: GraphType.VERTICAL_BAR,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.SUM,
groupByFieldMetadataIdX: TEST_FIELD_METADATA_ID_2,
orderByX: 'INVALID_ORDER' as any,
primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
primaryAxisOrderBy: 'INVALID_ORDER' as any,
};
export const INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY = {
graphType: GraphType.HORIZONTAL_BAR,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.SUM,
};
export const INVALID_HORIZONTAL_BAR_CHART_CONFIG_BAD_ORDER_BY = {
graphType: GraphType.HORIZONTAL_BAR,
aggregateFieldMetadataId: TEST_FIELD_METADATA_ID_1,
aggregateOperation: AggregateOperations.SUM,
primaryAxisGroupByFieldMetadataId: TEST_FIELD_METADATA_ID_2,
primaryAxisOrderBy: 'INVALID_ORDER' as any,
};
export const CONFIG_TYPE_MISMATCH_IFRAME_WITH_GRAPH = {
@@ -189,7 +228,8 @@ export const CONFIG_TYPE_MISMATCH_GRAPH_WITH_IFRAME = {
export const ALL_VALID_GRAPH_CONFIGS = [
TEST_NUMBER_CHART_CONFIG,
TEST_BAR_CHART_CONFIG,
TEST_VERTICAL_BAR_CHART_CONFIG,
TEST_HORIZONTAL_BAR_CHART_CONFIG,
TEST_LINE_CHART_CONFIG,
TEST_PIE_CHART_CONFIG,
TEST_GAUGE_CHART_CONFIG,
@@ -197,7 +237,8 @@ export const ALL_VALID_GRAPH_CONFIGS = [
export const ALL_MINIMAL_GRAPH_CONFIGS = [
TEST_NUMBER_CHART_CONFIG_MINIMAL,
TEST_BAR_CHART_CONFIG_MINIMAL,
TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
TEST_LINE_CHART_CONFIG_MINIMAL,
TEST_PIE_CHART_CONFIG_MINIMAL,
TEST_GAUGE_CHART_CONFIG_MINIMAL,
@@ -210,8 +251,10 @@ export const ALL_INVALID_CONFIGS = [
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
INVALID_NUMBER_CHART_CONFIG_INVALID_OPERATION,
INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_BAR_CHART_CONFIG_BAD_ORDER_BY,
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_VERTICAL_BAR_CHART_CONFIG_BAD_ORDER_BY,
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_HORIZONTAL_BAR_CHART_CONFIG_BAD_ORDER_BY,
];
export function getValidConfigForWidgetType(widgetType: string): any {
@@ -1,5 +1,5 @@
import {
TEST_BAR_CHART_CONFIG_MINIMAL,
TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
TEST_IFRAME_CONFIG,
TEST_IFRAME_CONFIG_ALTERNATIVE,
TEST_NUMBER_CHART_CONFIG_MINIMAL,
@@ -215,7 +215,7 @@ describe('Page Layout Update With Tabs And Widgets Integration', () => {
rowSpan: 2,
columnSpan: 3,
},
configuration: TEST_BAR_CHART_CONFIG_MINIMAL,
configuration: TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
},
{
id: newWidgetId,
@@ -0,0 +1,27 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconChartBar } from '@ui/display/icon/components/TablerIcons';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
const StyledRotatedIconWrapper = styled.div`
display: inline-flex;
transform: rotate(90deg);
`;
type IconChartBarHorizontalProps = Pick<
IconComponentProps,
'size' | 'stroke' | 'color'
>;
export const IconChartBarHorizontal = (props: IconChartBarHorizontalProps) => {
const theme = useTheme();
const size = props.size ?? theme.icon.size.md;
const stroke = props.stroke ?? theme.icon.stroke.sm;
return (
<StyledRotatedIconWrapper>
<IconChartBar size={size} stroke={stroke} color={props.color} />
</StyledRotatedIconWrapper>
);
};
+1
View File
@@ -27,6 +27,7 @@ export type {
} from './color/components/ColorSample';
export { ColorSample } from './color/components/ColorSample';
export { IconAddressBook } from './icon/components/IconAddressBook';
export { IconChartBarHorizontal } from './icon/components/IconChartBarHorizontal';
export { IconGmail } from './icon/components/IconGmail';
export { IconGoogle } from './icon/components/IconGoogle';
export { IconGoogleCalendar } from './icon/components/IconGoogleCalendar';