From 436084f8f4aa856f997c90f5891b411ef8e9969e Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:08:48 +0530 Subject: [PATCH] Split bar graph into two distinct horizontal and vertical bars (#15061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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 --- .../src/generated-metadata/graphql.ts | 29 +- .../twenty-front/src/generated/graphql.ts | 57 +- .../components/ChartTypeSelectionSection.tsx | 3 +- .../CommandMenuPageLayoutWidgetTypeSelect.tsx | 2 +- .../ChartDataSourceDropdownContent.tsx | 10 +- ...onForAggregateOperationDropdownContent.tsx | 17 +- ...rtGroupByFieldSelectionDropdownContent.tsx | 4 +- ...ChartSortByGroupByFieldDropdownContent.tsx | 31 +- ...hartXAxisFieldSelectionDropdownContent.tsx | 4 +- ...artXAxisSortBySelectionDropdownContent.tsx | 40 +- .../constants/GraphTypeInformation.ts | 14 +- .../constants/GraphTypeToConfigTypename.ts | 3 +- .../__tests__/useChartSettingsValues.test.tsx | 592 ++++++++++++++++++ .../hooks/useChartSettingsValues.ts | 103 +-- .../__tests__/getBarChartSettings.test.ts | 134 ++++ .../getBarChartSettings.ts} | 75 ++- .../utils/getFieldLabelWithSubField.ts | 10 +- .../PageLayoutRenderer.stories.tsx | 6 +- .../fragments/pageLayoutWidgetFragment.ts | 24 +- .../useCreatePageLayoutGraphWidget.test.tsx | 8 +- .../usePageLayoutDraftState.test.tsx | 6 +- .../extractFieldMetadataIdsFromWidget.test.ts | 26 +- .../utils/createDefaultGraphWidget.ts | 29 +- .../extractFieldMetadataIdsFromWidget.ts | 10 +- .../page-layout/utils/getDefaultWidgetData.ts | 23 +- .../__stories__/WidgetRenderer.stories.tsx | 24 +- .../widgets/graph/components/GraphWidget.tsx | 3 +- .../GraphWidgetBarChartRenderer.tsx | 2 + .../hooks/useGraphBarChartWidgetData.ts | 1 + ...blesFromBarChartConfiguration.test.ts.snap | 29 +- ...VariablesFromBarChartConfiguration.test.ts | 110 +++- ...reChartConfigurationFieldsValidForQuery.ts | 22 +- ...QueryVariablesFromBarChartConfiguration.ts | 39 +- .../transformGroupByDataToBarChartData.ts | 42 +- ...formOneDimensionalGroupByToBarChartData.ts | 7 +- ...formTwoDimensionalGroupByToBarChartData.ts | 8 +- .../dtos/bar-chart-configuration.dto.ts | 17 +- .../dtos/line-chart-configuration.dto.ts | 12 +- .../dtos/widget-configuration.interface.ts | 3 +- .../page-layout/enums/graph-type.enum.ts | 3 +- ...ransform-widget-configuration.util.spec.ts | 58 +- ...and-transform-widget-configuration.util.ts | 3 +- .../get-page-layout-widget-data-seeds.util.ts | 39 +- .../page-layout-gql-fields.constants.ts | 20 +- ...idget-configuration-test-data.constants.ts | 91 ++- ...-with-tabs-and-widgets.integration-spec.ts | 4 +- .../components/IconChartBarHorizontal.tsx | 27 + packages/twenty-ui/src/display/index.ts | 1 + 48 files changed, 1433 insertions(+), 392 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/__tests__/useChartSettingsValues.test.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts rename packages/twenty-front/src/modules/command-menu/pages/page-layout/{constants/BarChartSettings.ts => utils/getBarChartSettings.ts} (50%) create mode 100644 packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.tsx diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index a9284046a68..52b16766f88 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -287,16 +287,16 @@ export type BarChartConfiguration = { displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; - groupByFieldMetadataIdX: Scalars['UUID']; - groupByFieldMetadataIdY?: Maybe; - groupBySubFieldNameX?: Maybe; - groupBySubFieldNameY?: Maybe; groupMode?: Maybe; omitNullValues?: Maybe; - orderByX?: Maybe; - orderByY?: Maybe; + primaryAxisGroupByFieldMetadataId: Scalars['UUID']; + primaryAxisGroupBySubFieldName?: Maybe; + primaryAxisOrderBy?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; + secondaryAxisGroupByFieldMetadataId?: Maybe; + secondaryAxisGroupBySubFieldName?: Maybe; + secondaryAxisOrderBy?: Maybe; }; /** 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; filter?: Maybe; graphType: GraphType; - groupByFieldMetadataIdX: Scalars['UUID']; - groupByFieldMetadataIdY?: Maybe; - groupBySubFieldNameX?: Maybe; - groupBySubFieldNameY?: Maybe; omitNullValues?: Maybe; - orderByX?: Maybe; - orderByY?: Maybe; + primaryAxisGroupByFieldMetadataId: Scalars['UUID']; + primaryAxisGroupBySubFieldName?: Maybe; + primaryAxisOrderBy?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; + secondaryAxisGroupByFieldMetadataId?: Maybe; + secondaryAxisGroupBySubFieldName?: Maybe; + secondaryAxisOrderBy?: Maybe; }; export type LinkMetadata = { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 365f8be3256..51abfe047d6 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -287,16 +287,16 @@ export type BarChartConfiguration = { displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; - groupByFieldMetadataIdX: Scalars['UUID']; - groupByFieldMetadataIdY?: Maybe; - groupBySubFieldNameX?: Maybe; - groupBySubFieldNameY?: Maybe; groupMode?: Maybe; omitNullValues?: Maybe; - orderByX?: Maybe; - orderByY?: Maybe; + primaryAxisGroupByFieldMetadataId: Scalars['UUID']; + primaryAxisGroupBySubFieldName?: Maybe; + primaryAxisOrderBy?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; + secondaryAxisGroupByFieldMetadataId?: Maybe; + secondaryAxisGroupBySubFieldName?: Maybe; + secondaryAxisOrderBy?: Maybe; }; /** 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; filter?: Maybe; graphType: GraphType; - groupByFieldMetadataIdX: Scalars['UUID']; - groupByFieldMetadataIdY?: Maybe; - groupBySubFieldNameX?: Maybe; - groupBySubFieldNameY?: Maybe; omitNullValues?: Maybe; - orderByX?: Maybe; - orderByY?: Maybe; + primaryAxisGroupByFieldMetadataId: Scalars['UUID']; + primaryAxisGroupBySubFieldName?: Maybe; + primaryAxisOrderBy?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; + secondaryAxisGroupByFieldMetadataId?: Maybe; + secondaryAxisGroupBySubFieldName?: Maybe; + secondaryAxisOrderBy?: Maybe; }; 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 diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx index 6a37494f568..e74241e8bd2 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartTypeSelectionSection.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx index 4a4f228b41f..34f1d79657c 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect.tsx @@ -34,7 +34,7 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => { if (!isDefined(pageLayoutEditingWidgetId)) { const fieldSelection = buildBarChartFieldSelection(); const newWidget = createPageLayoutGraphWidget({ - graphType: GraphType.BAR, + graphType: GraphType.VERTICAL_BAR, fieldSelection, }); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDataSourceDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDataSourceDropdownContent.tsx index 87924a4c093..f2c3d76cc60 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDataSourceDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartDataSourceDropdownContent.tsx @@ -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, }, diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx index 399001bae70..ee158e06608 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartFieldSelectionForAggregateOperationDropdownContent.tsx @@ -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 ( { - fieldMetadataIdKey="groupByFieldMetadataIdY" - subFieldNameKey="groupBySubFieldNameY" + fieldMetadataIdKey="secondaryAxisGroupByFieldMetadataId" + subFieldNameKey="secondaryAxisGroupBySubFieldName" /> ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortByGroupByFieldDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortByGroupByFieldDropdownContent.tsx index 2ddc074a8a0..e0c11941b1b 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortByGroupByFieldDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartSortByGroupByFieldDropdownContent.tsx @@ -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={() => { diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisFieldSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisFieldSelectionDropdownContent.tsx index 7f048136e74..9f58fb6eaf9 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisFieldSelectionDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisFieldSelectionDropdownContent.tsx @@ -9,8 +9,8 @@ export const ChartXAxisFieldSelectionDropdownContent = () => { - fieldMetadataIdKey="groupByFieldMetadataIdX" - subFieldNameKey="groupBySubFieldNameX" + fieldMetadataIdKey="primaryAxisGroupByFieldMetadataId" + subFieldNameKey="primaryAxisGroupBySubFieldName" /> ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisSortBySelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisSortBySelectionDropdownContent.tsx index 86080ee4703..c15e2b9948c 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisSortBySelectionDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartXAxisSortBySelectionDropdownContent.tsx @@ -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 ( { key={sortOption.value} itemId={sortOption.value} onEnter={() => { - handleSelectSortOption(sortOption.value); + handleSelect(sortOption.value); }} > { - handleSelectSortOption(sortOption.value); + handleSelect(sortOption.value); }} /> diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeInformation.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeInformation.ts index 180d96f2cea..f24610a967f 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeInformation.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeInformation.ts @@ -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`, diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename.ts index 33121c2ae45..4c4955fa8b7 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/GraphTypeToConfigTypename.ts @@ -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', diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/__tests__/useChartSettingsValues.test.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/__tests__/useChartSettingsValues.test.tsx new file mode 100644 index 00000000000..ab8309925c4 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/__tests__/useChartSettingsValues.test.tsx @@ -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 => + ({ + __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 }) => ( + { + set(objectMetadataItemsState, [mockObjectMetadataItem]); + }} + > + {children} + + ), + }, + ); +}; + +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); + + 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 }) => ( + { + set(objectMetadataItemsState, [mockObjectMetadataItem]); + }} + > + {children} + + ), + }, + ); + + 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); + }); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts index 7931607475c..4cfe58b07ea 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts @@ -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); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts new file mode 100644 index 00000000000..b8e389982b2 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts @@ -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, + ]); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/BarChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts similarity index 50% rename from packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/BarChartSettings.ts rename to packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts index 9e951200d87..f10d2d3f836 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/BarChartSettings.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts @@ -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, + ], + }, + ]; +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getFieldLabelWithSubField.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getFieldLabelWithSubField.ts index 90a218f25ef..9166c7bde49 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getFieldLabelWithSubField.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getFieldLabelWithSubField.ts @@ -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) diff --git a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx index d238d546b80..6e6ecd03c36 100644 --- a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutRenderer.stories.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts index 9fd4f90c443..59e9ae84836 100644 --- a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts +++ b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts @@ -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 diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx index 35993ec9653..b64ba4c08aa 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/useCreatePageLayoutGraphWidget.test.tsx @@ -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'); }); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx index 5e3a8e7eb11..bf9c6e96d46 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx +++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutDraftState.test.tsx @@ -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, diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/extractFieldMetadataIdsFromWidget.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/extractFieldMetadataIdsFromWidget.test.ts index ec374d2e551..4c2b21036ec 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/extractFieldMetadataIdsFromWidget.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/extractFieldMetadataIdsFromWidget.test.ts @@ -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, }, diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts index 3e09c1b6866..47d1775705a 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts @@ -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, }; diff --git a/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts index 632cd13205c..bc0288a5b08 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts @@ -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': diff --git a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts index f9f883b5a0f..1b9859f802c 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts @@ -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 }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx index a00316119fe..5bc9df959dd 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/__stories__/WidgetRenderer.stories.tsx @@ -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, }, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx index 2083ebb2890..48e65f1841d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx @@ -100,7 +100,8 @@ export const GraphWidget = ({ ); - case GraphType.BAR: + case GraphType.VERTICAL_BAR: + case GraphType.HORIZONTAL_BAR: return ; case GraphType.LINE: diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx index 41db1536dc3..e113a0d4567 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx @@ -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" diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts index 952f0e18117..d777a5829d6 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts @@ -19,6 +19,7 @@ type UseGraphBarChartWidgetDataResult = { xAxisLabel?: string; yAxisLabel?: string; showDataLabels: boolean; + layout?: 'vertical' | 'horizontal'; loading: boolean; error?: Error; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/generateGroupByQueryVariablesFromBarChartConfiguration.test.ts.snap b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/generateGroupByQueryVariablesFromBarChartConfiguration.test.ts.snap index 14afcd0ef6d..0aa4515a47e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/generateGroupByQueryVariablesFromBarChartConfiguration.test.ts.snap +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/generateGroupByQueryVariablesFromBarChartConfiguration.test.ts.snap @@ -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": [ { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromBarChartConfiguration.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromBarChartConfiguration.test.ts index b0e5f8d2407..8a0f8b1d13d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromBarChartConfiguration.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/generateGroupByQueryVariablesFromBarChartConfiguration.test.ts @@ -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 => + ({ + __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'); }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts index 3168a808998..8a37939db3d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts @@ -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 ( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration.ts index 11c5e9c6372..6e2d226ccbe 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration.ts @@ -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> = []; // 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!, // ), // }); // } diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData.ts index de0eefc2fa2..31707a91052 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToBarChartData.ts @@ -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, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts index f919b87546f..1f76bd8d29c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts @@ -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, }) : ''; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts index 36cd99749c0..c5d6181eb22 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts @@ -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(); @@ -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) diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts index d34d08eac11..ac4c3f71f05 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts index 4dcc02f9be1..726aee07110 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts index e1e6b17ad23..7aef50003fa 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/widget-configuration.interface.ts @@ -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; diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-type.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-type.enum.ts index 413fec91067..394d5fcfeed 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-type.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/graph-type.enum.ts @@ -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', } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts index 7fa3be1e78f..4957240ca7a 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/__tests__/validate-and-transform-widget-configuration.util.spec.ts @@ -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/); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts index c9c6b0d84ec..c72461ee239 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util.ts @@ -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, { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts index fbce7315d08..e92d5369ba7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/get-page-layout-widget-data-seeds.util.ts @@ -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, } diff --git a/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts b/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts index 0be50ba5e78..1aafe25de01 100644 --- a/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts +++ b/packages/twenty-server/test/integration/constants/page-layout-gql-fields.constants.ts @@ -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 diff --git a/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts index 0a34d7cab89..3d01c60c4fd 100644 --- a/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts +++ b/packages/twenty-server/test/integration/constants/widget-configuration-test-data.constants.ts @@ -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 { diff --git a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts index 6d2fdf848d8..a39e45a0035 100644 --- a/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/page-layout/page-layout-update-with-tabs-and-widgets.integration-spec.ts @@ -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, diff --git a/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.tsx b/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.tsx new file mode 100644 index 00000000000..7d61c2b5aba --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconChartBarHorizontal.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index cf9fe6dac51..948c662d1e0 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -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';