From 44f97058ac86ce5e2569d61d70c8c071aeaacc39 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Fri, 10 Oct 2025 02:04:51 +0530 Subject: [PATCH] Tidy up validation for configuration (#14939) closes https://github.com/twentyhq/core-team-issues/issues/1605 TODO: ~~- add stories~~ ~~- add base graph on companies when creating a new graph widget~~ --- .../src/generated-metadata/graphql.ts | 21 +- .../twenty-front/src/generated/graphql.ts | 25 +- .../CommandMenuPageLayoutWidgetTypeSelect.tsx | 9 +- .../ChartColorSelectionDropdownContent.tsx | 4 + ...ChartSortByGroupByFieldDropdownContent.tsx | 15 +- ...artXAxisSortBySelectionDropdownContent.tsx | 5 +- .../hooks/useChartSettingsValues.ts | 25 +- .../PageLayoutRenderer.stories.tsx | 29 +- .../useCreatePageLayoutGraphWidget.test.tsx | 44 ++- .../hooks/useCompanyDefaultChartConfig.ts | 76 ++++ .../hooks/useCreatePageLayoutGraphWidget.ts | 10 +- .../types/GraphWidgetFieldSelection.ts | 6 + .../utils/createDefaultGraphWidget.ts | 105 +++--- .../extractFieldMetadataIdsFromWidget.ts | 15 +- .../page-layout/utils/getDefaultWidgetData.ts | 2 +- .../PageLayoutWidgetNoDataDisplay.tsx | 25 ++ .../widgets/components/WidgetRenderer.tsx | 8 +- .../__stories__/WidgetRenderer.stories.tsx | 324 +++++++++++++++--- .../widgets/graph/components/GraphWidget.tsx | 133 +++++++ .../graph/components/GraphWidgetRenderer.tsx | 128 +------ ...reChartConfigurationFieldsValidForQuery.ts | 70 ++++ .../transformGroupByDataToBarChartData.ts | 2 +- .../controllers/page-layout-tab.controller.ts | 2 +- .../page-layout-widget.controller.ts | 2 +- .../controllers/page-layout.controller.ts | 2 +- .../dtos/bar-chart-configuration.dto.ts | 21 +- .../dtos/gauge-chart-configuration.dto.ts | 5 +- .../dtos/line-chart-configuration.dto.ts | 24 +- .../dtos/number-chart-configuration.dto.ts | 11 +- .../dtos/pie-chart-configuration.dto.ts | 15 +- .../dtos/widget-configuration.interface.ts | 45 ++- .../enums/widget-configuration-type.enum.ts | 6 +- .../resolvers/page-layout-widget.resolver.ts | 19 +- ...ransform-widget-configuration.util.spec.ts | 42 +-- ...widget-configuration-discriminator.util.ts | 32 ++ ...and-transform-widget-configuration.util.ts | 61 ++-- .../page-layout-gql-fields.constants.ts | 1 - ...idget-configuration-test-data.constants.ts | 1 - .../page-layout-tab.integration-spec.ts | 36 +- .../page-layout-widget.integration-spec.ts | 38 +- .../suites/page-layout.integration-spec.ts | 34 +- .../rest/utils/page-layout-rest-api.util.ts | 4 +- .../utils/page-layout-tab-rest-api.util.ts | 4 +- .../utils/page-layout-widget-rest-api.util.ts | 4 +- 44 files changed, 1015 insertions(+), 475 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/useCompanyDefaultChartConfig.ts create mode 100644 packages/twenty-front/src/modules/page-layout/types/GraphWidgetFieldSelection.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts create mode 100644 packages/twenty-server/src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index da01ebd2db..a5b02037ea 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -281,10 +281,10 @@ export type BarChartConfiguration = { __typename?: 'BarChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; aggregateOperation: ExtendedAggregateOperations; - axisNameDisplay: AxisNameDisplay; + axisNameDisplay?: Maybe; color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; groupByFieldMetadataIdX: Scalars['UUID']; @@ -292,7 +292,7 @@ export type BarChartConfiguration = { groupBySubFieldNameX?: Maybe; groupBySubFieldNameY?: Maybe; omitNullValues?: Maybe; - orderByX: GraphOrderBy; + orderByX?: Maybe; orderByY?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; @@ -1368,7 +1368,7 @@ export type GaugeChartConfiguration = { aggregateOperation: ExtendedAggregateOperations; color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; }; @@ -1593,10 +1593,10 @@ export type LineChartConfiguration = { __typename?: 'LineChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; aggregateOperation: ExtendedAggregateOperations; - axisNameDisplay: AxisNameDisplay; + axisNameDisplay?: Maybe; color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; groupByFieldMetadataIdX: Scalars['UUID']; @@ -1604,7 +1604,7 @@ export type LineChartConfiguration = { groupBySubFieldNameX?: Maybe; groupBySubFieldNameY?: Maybe; omitNullValues?: Maybe; - orderByX: GraphOrderBy; + orderByX?: Maybe; orderByY?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; @@ -2709,9 +2709,8 @@ export type NumberChartConfiguration = { __typename?: 'NumberChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; aggregateOperation: ExtendedAggregateOperations; - color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; format?: Maybe; graphType: GraphType; @@ -2956,12 +2955,12 @@ export type PieChartConfiguration = { aggregateOperation: ExtendedAggregateOperations; color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; groupByFieldMetadataId: Scalars['UUID']; groupBySubFieldName?: Maybe; - orderBy: GraphOrderBy; + orderBy?: Maybe; }; export type PlaceDetailsResultDto = { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index aa788b415b..b63437b443 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -281,10 +281,10 @@ export type BarChartConfiguration = { __typename?: 'BarChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; aggregateOperation: ExtendedAggregateOperations; - axisNameDisplay: AxisNameDisplay; + axisNameDisplay?: Maybe; color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; groupByFieldMetadataIdX: Scalars['UUID']; @@ -292,7 +292,7 @@ export type BarChartConfiguration = { groupBySubFieldNameX?: Maybe; groupBySubFieldNameY?: Maybe; omitNullValues?: Maybe; - orderByX: GraphOrderBy; + orderByX?: Maybe; orderByY?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; @@ -1325,7 +1325,7 @@ export type GaugeChartConfiguration = { aggregateOperation: ExtendedAggregateOperations; color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; }; @@ -1550,10 +1550,10 @@ export type LineChartConfiguration = { __typename?: 'LineChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; aggregateOperation: ExtendedAggregateOperations; - axisNameDisplay: AxisNameDisplay; + axisNameDisplay?: Maybe; color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; groupByFieldMetadataIdX: Scalars['UUID']; @@ -1561,7 +1561,7 @@ export type LineChartConfiguration = { groupBySubFieldNameX?: Maybe; groupBySubFieldNameY?: Maybe; omitNullValues?: Maybe; - orderByX: GraphOrderBy; + orderByX?: Maybe; orderByY?: Maybe; rangeMax?: Maybe; rangeMin?: Maybe; @@ -2620,9 +2620,8 @@ export type NumberChartConfiguration = { __typename?: 'NumberChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; aggregateOperation: ExtendedAggregateOperations; - color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; format?: Maybe; graphType: GraphType; @@ -2867,12 +2866,12 @@ export type PieChartConfiguration = { aggregateOperation: ExtendedAggregateOperations; color?: Maybe; description?: Maybe; - displayDataLabel: Scalars['Boolean']; + displayDataLabel?: Maybe; filter?: Maybe; graphType: GraphType; groupByFieldMetadataId: Scalars['UUID']; groupBySubFieldName?: Maybe; - orderBy: GraphOrderBy; + orderBy?: Maybe; }; export type PlaceDetailsResultDto = { @@ -4432,7 +4431,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, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, 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, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, 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, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy: GraphOrderBy, displayDataLabel: boolean, 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, 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?: '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 UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ id: Scalars['String']; @@ -4440,7 +4439,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, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, displayDataLabel: boolean, 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, groupByFieldMetadataIdY?: any | null, groupBySubFieldNameY?: string | null, orderByY?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay: AxisNameDisplay, displayDataLabel: boolean, 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, description?: string | null, filter?: any | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: ExtendedAggregateOperations, groupBySubFieldName?: string | null, orderBy: GraphOrderBy, displayDataLabel: boolean, 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, 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?: '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 OnDbEventSubscriptionVariables = Exact<{ input: OnDbEventInput; 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 764d49fcbd..4a4f228b41 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 @@ -4,6 +4,7 @@ import { CommandMenuList } from '@/command-menu/components/CommandMenuList'; import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useCompanyDefaultChartConfig } from '@/page-layout/hooks/useCompanyDefaultChartConfig'; import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; @@ -18,6 +19,8 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => { const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu(); + const { buildBarChartFieldSelection } = useCompanyDefaultChartConfig(); + const { createPageLayoutGraphWidget } = useCreatePageLayoutGraphWidget(pageLayoutId); @@ -29,7 +32,11 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => { const handleNavigateToGraphTypeSelect = () => { if (!isDefined(pageLayoutEditingWidgetId)) { - const newWidget = createPageLayoutGraphWidget(GraphType.BAR); + const fieldSelection = buildBarChartFieldSelection(); + const newWidget = createPageLayoutGraphWidget({ + graphType: GraphType.BAR, + fieldSelection, + }); setPageLayoutEditingWidgetId(newWidget.id); } diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx index 39e385f715..e53c966f8b 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartColorSelectionDropdownContent.tsx @@ -48,6 +48,10 @@ export const ChartColorSelectionDropdownContent = () => { const configuration = widgetInEditMode.configuration as ChartConfiguration; + if (!('color' in configuration)) { + return null; + } + const currentColor = configuration.color; const colorOptions = MAIN_COLOR_NAMES.map((colorName) => ({ 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 2f14a54976..2ddc074a8a 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 @@ -17,7 +17,6 @@ import { type BarChartConfiguration, type GraphOrderBy, type LineChartConfiguration, - type NumberChartConfiguration, } from '~/generated/graphql'; export const ChartSortByGroupByFieldDropdownContent = () => { @@ -26,15 +25,9 @@ export const ChartSortByGroupByFieldDropdownContent = () => { const configuration = widgetInEditMode?.configuration as | BarChartConfiguration - | LineChartConfiguration - | NumberChartConfiguration; + | LineChartConfiguration; - const currentOrderBy = - 'orderByY' in configuration - ? configuration.orderByY - : 'orderBy' in configuration - ? configuration.orderBy - : undefined; + const currentOrderBy = configuration.orderByY; const dropdownId = useAvailableComponentInstanceIdOrThrow( DropdownComponentInstanceContext, @@ -50,12 +43,10 @@ export const ChartSortByGroupByFieldDropdownContent = () => { const { closeDropdown } = useCloseDropdown(); - const orderByKey = 'orderByY' in configuration ? 'orderByY' : 'orderBy'; - const handleSelectSortOption = (orderBy: GraphOrderBy) => { updateCurrentWidgetConfig({ configToUpdate: { - [orderByKey]: orderBy, + orderByY: orderBy, }, }); closeDropdown(); 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 90c19d0704..86080ee470 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 @@ -76,8 +76,9 @@ export const ChartXAxisSortBySelectionDropdownContent = () => { graphOrderBy: sortOption.value, groupByFieldMetadataIdX: configuration.groupByFieldMetadataIdX, aggregateFieldMetadataId: - configuration.aggregateFieldMetadataId, - aggregateOperation: configuration.aggregateOperation, + configuration.aggregateFieldMetadataId ?? undefined, + aggregateOperation: + configuration.aggregateOperation ?? undefined, })} selected={currentOrderByX === sortOption.value} focused={selectedItemId === sortOption.value} 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 78b109e2ea..0d193fe26b 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 @@ -80,7 +80,7 @@ export const useChartSettingsValues = ({ groupBySubFieldNameX: configuration.groupBySubFieldNameX as CompositeFieldSubFieldName, aggregateFieldMetadataId: configuration.aggregateFieldMetadataId, - aggregateOperation: configuration.aggregateOperation, + aggregateOperation: configuration.aggregateOperation ?? undefined, }) : undefined; @@ -116,17 +116,26 @@ export const useChartSettingsValues = ({ case CHART_CONFIGURATION_SETTING_IDS.SOURCE: return objectMetadataItem?.labelPlural; case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X: - return groupBySubFieldNameXLabel; + return groupBySubFieldNameXLabel ?? groupByFieldX?.label; case CHART_CONFIGURATION_SETTING_IDS.COLORS: - return isDefined(configuration.color) && 'color' in configuration - ? capitalize(configuration.color) + return 'color' in configuration && isDefined(configuration.color) + ? capitalize(configuration.color as string) : undefined; - case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y: - return `${aggregateField?.label ?? ''}${aggregateField?.label ? ` (${getAggregateOperationLabel(yAxisAggregateOperation)})` : ''}`; + case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y: { + const hasAggregateLabel = isDefined(aggregateField?.label); + const hasAggregateOperation = isDefined(yAxisAggregateOperation); + + return `${aggregateField?.label ?? ''}${ + hasAggregateLabel && hasAggregateOperation + ? ` (${getAggregateOperationLabel(yAxisAggregateOperation)})` + : '' + }`; + } case CHART_CONFIGURATION_SETTING_IDS.GROUP_BY: return groupByFieldY?.label; case CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME: - return 'axisNameDisplay' in configuration + return 'axisNameDisplay' in configuration && + isDefined(configuration.axisNameDisplay) ? getChartAxisNameDisplayOptions(configuration.axisNameDisplay) : undefined; case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_X: @@ -134,7 +143,7 @@ export const useChartSettingsValues = ({ case CHART_CONFIGURATION_SETTING_IDS.SORT_BY_GROUP_BY_FIELD: return groupByOrderByLabel; case CHART_CONFIGURATION_SETTING_IDS.DATA_LABELS: - return configuration.displayDataLabel; + return configuration.displayDataLabel ?? undefined; default: return ''; } 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 8a67788dc7..d238d546b8 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 @@ -25,9 +25,22 @@ import { type PageLayoutWidget, } from '~/generated/graphql'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow'; import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow'; const mockPersonObjectMetadataItem = getMockObjectMetadataItemOrThrow('person'); +const idField = getMockFieldMetadataItemOrThrow({ + objectMetadataItem: mockPersonObjectMetadataItem, + fieldName: 'id', +}); +const nameField = getMockFieldMetadataItemOrThrow({ + objectMetadataItem: mockPersonObjectMetadataItem, + fieldName: 'name', +}); +const createdAtField = getMockFieldMetadataItemOrThrow({ + objectMetadataItem: mockPersonObjectMetadataItem, + fieldName: 'createdAt', +}); const validatePageLayoutContent = async (canvasElement: HTMLElement) => { const canvas = within(canvasElement); @@ -76,7 +89,7 @@ const mixedGraphsPageLayoutMocks = { __typename: 'NumberChartConfiguration', graphType: GraphType.NUMBER, aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: 'id', + aggregateFieldMetadataId: idField.id, }, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', @@ -100,7 +113,7 @@ const mixedGraphsPageLayoutMocks = { __typename: 'GaugeChartConfiguration', graphType: GraphType.GAUGE, aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: 'id', + aggregateFieldMetadataId: idField.id, displayDataLabel: false, }, createdAt: '2024-01-01T00:00:00Z', @@ -125,8 +138,8 @@ const mixedGraphsPageLayoutMocks = { __typename: 'PieChartConfiguration', graphType: GraphType.PIE, aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: 'id', - groupByFieldMetadataId: 'createdAt', + aggregateFieldMetadataId: idField.id, + groupByFieldMetadataId: createdAtField.id, orderBy: GraphOrderBy.VALUE_DESC, }, createdAt: '2024-01-01T00:00:00Z', @@ -151,12 +164,8 @@ const mixedGraphsPageLayoutMocks = { __typename: 'BarChartConfiguration', graphType: GraphType.BAR, aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: mockPersonObjectMetadataItem.fields.find( - (field) => field.name === 'name', - )?.id, - groupByFieldMetadataIdX: mockPersonObjectMetadataItem.fields.find( - (field) => field.name === 'createdAt', - )?.id, + aggregateFieldMetadataId: nameField.id, + groupByFieldMetadataIdX: createdAtField.id, orderByX: GraphOrderBy.FIELD_ASC, axisNameDisplay: AxisNameDisplay.BOTH, displayDataLabel: false, 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 a75a54ef3d..35993ec965 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 @@ -6,12 +6,14 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { act, renderHook } from '@testing-library/react'; import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; import { GraphType, WidgetType } from '~/generated-metadata/graphql'; import { PageLayoutType } from '~/generated/graphql'; import { PAGE_LAYOUT_TEST_INSTANCE_ID, PageLayoutTestWrapper, } from './PageLayoutTestWrapper'; +import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection'; jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid'), @@ -82,7 +84,9 @@ describe('useCreatePageLayoutGraphWidget', () => { }); act(() => { - result.current.createWidget.createPageLayoutGraphWidget(GraphType.BAR); + result.current.createWidget.createPageLayoutGraphWidget({ + graphType: GraphType.BAR, + }); }); expect(result.current.allWidgets).toHaveLength(1); @@ -162,9 +166,28 @@ describe('useCreatePageLayoutGraphWidget', () => { GraphType.BAR, ]; + const mockFieldSelections: Partial< + Record + > = { + [GraphType.NUMBER]: { + objectMetadataId: 'test-object-id', + aggregateFieldMetadataId: 'test-aggregate-field-id', + }, + [GraphType.BAR]: { + objectMetadataId: 'test-object-id', + groupByFieldMetadataIdX: 'test-groupby-field-id', + aggregateFieldMetadataId: 'test-aggregate-field-id', + }, + }; + graphTypes.forEach((graphType) => { act(() => { - result.current.createWidget.createPageLayoutGraphWidget(graphType); + const fieldSelection = + mockFieldSelections[graphType as keyof typeof mockFieldSelections]; + result.current.createWidget.createPageLayoutGraphWidget({ + graphType, + fieldSelection, + }); }); }); @@ -174,11 +197,14 @@ describe('useCreatePageLayoutGraphWidget', () => { const widget = result.current.allWidgets[index]; expect(widget.type).toBe(WidgetType.GRAPH); expect(widget.pageLayoutTabId).toBe('tab-1'); - expect( - widget.configuration && 'graphType' in widget.configuration - ? widget.configuration.graphType - : null, - ).toBe(graphType); + + if ( + isDefined(widget.configuration) && + 'graphType' in widget.configuration + ) { + expect(widget.configuration.graphType).toBe(graphType); + } + expect(widget.id).toBe('mock-uuid'); }); @@ -207,7 +233,9 @@ describe('useCreatePageLayoutGraphWidget', () => { ); expect(() => { - result.current.createWidget.createPageLayoutGraphWidget(GraphType.BAR); + result.current.createWidget.createPageLayoutGraphWidget({ + graphType: GraphType.BAR, + }); }).toThrow('A tab must be selected to create a new graph widget'); }); }); diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCompanyDefaultChartConfig.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCompanyDefaultChartConfig.ts new file mode 100644 index 0000000000..7aa22e51fd --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCompanyDefaultChartConfig.ts @@ -0,0 +1,76 @@ +import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection'; + +export const useCompanyDefaultChartConfig = () => { + const companyObjectMetadata = useRecoilValue( + objectMetadataItemFamilySelector({ + objectName: 'company', + objectNameType: 'singular', + }), + ); + + const buildBarChartFieldSelection = (): + | GraphWidgetFieldSelection + | undefined => { + if (!isDefined(companyObjectMetadata)) { + return; + } + + const labelIdentifierField = companyObjectMetadata.fields.find( + (field) => + field.id === companyObjectMetadata.labelIdentifierFieldMetadataId, + ); + + const employeesField = companyObjectMetadata.fields.find( + (field) => field.name === 'employees', + ); + + const arrField = companyObjectMetadata.fields.find( + (field) => field.name === 'annualRecurringRevenue', + ); + + const aggregateField = employeesField ?? arrField; + + if (!isDefined(labelIdentifierField) || !isDefined(aggregateField)) { + return; + } + + return { + objectMetadataId: companyObjectMetadata.id, + groupByFieldMetadataIdX: labelIdentifierField.id, + aggregateFieldMetadataId: aggregateField.id, + }; + }; + + const buildNumberChartFieldSelection = (): + | GraphWidgetFieldSelection + | undefined => { + if (!isDefined(companyObjectMetadata)) { + return; + } + + const employeesField = companyObjectMetadata.fields.find( + (field) => field.name === 'employees', + ); + + const arrField = companyObjectMetadata.fields.find( + (field) => field.name === 'annualRecurringRevenue', + ); + + const aggregateField = employeesField ?? arrField; + + if (!isDefined(aggregateField)) { + return; + } + + return { + objectMetadataId: companyObjectMetadata.id, + aggregateFieldMetadataId: aggregateField.id, + }; + }; + + return { buildBarChartFieldSelection, buildNumberChartFieldSelection }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts index d8456664a8..49addd55ef 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts @@ -2,6 +2,7 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; +import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget'; import { @@ -52,7 +53,13 @@ export const useCreatePageLayoutGraphWidget = ( const createPageLayoutGraphWidget = useRecoilCallback( ({ snapshot, set }) => - (graphType: GraphType): PageLayoutWidget => { + ({ + graphType, + fieldSelection, + }: { + graphType: GraphType; + fieldSelection?: GraphWidgetFieldSelection; + }): PageLayoutWidget => { const activeTabId = snapshot.getLoadable(activeTabIdState).getValue(); if (!isDefined(activeTabId)) { @@ -101,6 +108,7 @@ export const useCreatePageLayoutGraphWidget = ( rowSpan: position.h, columnSpan: position.w, }, + fieldSelection, }); const newLayout = { diff --git a/packages/twenty-front/src/modules/page-layout/types/GraphWidgetFieldSelection.ts b/packages/twenty-front/src/modules/page-layout/types/GraphWidgetFieldSelection.ts new file mode 100644 index 0000000000..784963bb57 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/types/GraphWidgetFieldSelection.ts @@ -0,0 +1,6 @@ +export type GraphWidgetFieldSelection = { + objectMetadataId?: string; + aggregateFieldMetadataId?: string; + groupByFieldMetadataId?: string; + groupByFieldMetadataIdX?: string; +}; 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 f39e60245d..3e09c1b686 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultGraphWidget.ts @@ -1,85 +1,80 @@ -import { assertUnreachable } from 'twenty-shared/utils'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { type ThemeColor } from 'twenty-ui/theme'; -import { v4 as uuidv4 } from 'uuid'; -import { GraphOrderBy, GraphType } from '~/generated-metadata/graphql'; +import { + ExtendedAggregateOperations, + GraphOrderBy, + GraphType, +} from '~/generated-metadata/graphql'; import { AxisNameDisplay, - ExtendedAggregateOperations, type GridPosition, type PageLayoutWidget, type WidgetConfiguration, WidgetType, } from '~/generated/graphql'; +import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection'; + const createDefaultGraphConfiguration = ( graphType: GraphType, -): WidgetConfiguration => { - const placeholderFieldId1 = uuidv4(); - const placeholderFieldId2 = uuidv4(); - + fieldSelection?: GraphWidgetFieldSelection, +): WidgetConfiguration | null => { switch (graphType) { case GraphType.NUMBER: + if (!isDefined(fieldSelection?.aggregateFieldMetadataId)) { + return null; + } return { __typename: 'NumberChartConfiguration', graphType: GraphType.NUMBER, + aggregateFieldMetadataId: fieldSelection.aggregateFieldMetadataId, aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: placeholderFieldId1, - displayDataLabel: false, + displayDataLabel: true, }; case GraphType.PIE: - return { - __typename: 'PieChartConfiguration', - graphType: GraphType.PIE, - aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: placeholderFieldId1, - groupByFieldMetadataId: placeholderFieldId2, - orderBy: GraphOrderBy.VALUE_DESC, - displayDataLabel: false, - color: 'blue' satisfies ThemeColor, - }; + return null; case GraphType.BAR: + if ( + !isDefined(fieldSelection?.aggregateFieldMetadataId) || + !isDefined(fieldSelection?.groupByFieldMetadataIdX) + ) { + return null; + } return { __typename: 'BarChartConfiguration', graphType: GraphType.BAR, - aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: placeholderFieldId1, - groupByFieldMetadataIdX: placeholderFieldId2, - orderByX: GraphOrderBy.FIELD_ASC, displayDataLabel: false, - axisNameDisplay: AxisNameDisplay.BOTH, color: 'blue' satisfies ThemeColor, + groupByFieldMetadataIdX: fieldSelection.groupByFieldMetadataIdX, + aggregateFieldMetadataId: fieldSelection.aggregateFieldMetadataId, + aggregateOperation: ExtendedAggregateOperations.SUM, + orderByX: GraphOrderBy.FIELD_ASC, + axisNameDisplay: AxisNameDisplay.BOTH, }; case GraphType.LINE: - return { - __typename: 'LineChartConfiguration', - graphType: GraphType.LINE, - aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: placeholderFieldId1, - groupByFieldMetadataIdX: placeholderFieldId2, - orderByX: GraphOrderBy.FIELD_ASC, - displayDataLabel: false, - axisNameDisplay: AxisNameDisplay.BOTH, - color: 'blue' satisfies ThemeColor, - }; + return null; case GraphType.GAUGE: - return { - __typename: 'GaugeChartConfiguration', - graphType: GraphType.GAUGE, - aggregateOperation: ExtendedAggregateOperations.COUNT, - aggregateFieldMetadataId: placeholderFieldId1, - displayDataLabel: false, - color: 'blue' satisfies ThemeColor, - }; + return null; default: assertUnreachable(graphType); } }; +type CreateDefaultGraphWidgetParams = { + id: string; + pageLayoutTabId: string; + title: string; + graphType: GraphType; + gridPosition: GridPosition; + objectMetadataId?: string | null; + fieldSelection?: GraphWidgetFieldSelection; +}; + export const createDefaultGraphWidget = ({ id, pageLayoutTabId, @@ -87,23 +82,25 @@ export const createDefaultGraphWidget = ({ graphType, gridPosition, objectMetadataId, -}: { - id: string; - pageLayoutTabId: string; - title: string; - graphType: GraphType; - gridPosition: GridPosition; - objectMetadataId?: string | null; -}): PageLayoutWidget => { + fieldSelection, +}: CreateDefaultGraphWidgetParams): PageLayoutWidget => { + const resolvedObjectMetadataId = + fieldSelection?.objectMetadataId ?? objectMetadataId ?? null; + + const configuration = createDefaultGraphConfiguration( + graphType, + fieldSelection, + ); + return { __typename: 'PageLayoutWidget', id, pageLayoutTabId, title, type: WidgetType.GRAPH, - configuration: createDefaultGraphConfiguration(graphType), + configuration, gridPosition, - objectMetadataId: objectMetadataId ?? null, + objectMetadataId: resolvedObjectMetadataId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), deletedAt: null, 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 77ef562ce1..632cd13205 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts @@ -19,19 +19,20 @@ export const extractFieldMetadataIdsFromWidget = ( return [ config.aggregateFieldMetadataId, config.groupByFieldMetadataIdX, - ...(isDefined(config.groupByFieldMetadataIdY) - ? [config.groupByFieldMetadataIdY] - : []), - ]; + config.groupByFieldMetadataIdY, + ].filter(isDefined); case 'PieChartConfiguration': - return [config.aggregateFieldMetadataId, config.groupByFieldMetadataId]; + return [ + config.aggregateFieldMetadataId, + config.groupByFieldMetadataId, + ].filter(isDefined); case 'NumberChartConfiguration': - return [config.aggregateFieldMetadataId]; + return [config.aggregateFieldMetadataId].filter(isDefined); case 'GaugeChartConfiguration': - return [config.aggregateFieldMetadataId]; + return [config.aggregateFieldMetadataId].filter(isDefined); case 'IframeConfiguration': return []; 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 203a3d37e9..f9f883b5a0 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/getDefaultWidgetData.ts @@ -135,7 +135,7 @@ export const getWidgetSize = (graphType: GraphType) => { case GraphType.PIE: return { w: 4, h: 4 }; case GraphType.BAR: - return { w: 6, h: 4 }; + return { w: 6, h: 6 }; case GraphType.LINE: return { w: 6, h: 10 }; default: diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay.tsx new file mode 100644 index 0000000000..665885d94d --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay.tsx @@ -0,0 +1,25 @@ +import { t } from '@lingui/core/macro'; +import { AppTooltip, Status } from 'twenty-ui/display'; + +type PageLayoutWidgetNoDataDisplayProps = { + widgetId: string; +}; + +export const PageLayoutWidgetNoDataDisplay = ({ + widgetId, +}: PageLayoutWidgetNoDataDisplayProps) => { + const tooltipId = `widget-incomplete-tooltip-${widgetId}`; + + return ( + <> +
+ +
+ + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx index a40b50a8ce..a423dbf628 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx @@ -25,19 +25,23 @@ const StyledContent = styled.div` export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { const { deletePageLayoutWidget } = useDeletePageLayoutWidget(); const { handleEditWidget } = useEditPageLayoutWidget(); - const { hasAccess, restriction } = useWidgetPermissions(widget); const isPageLayoutInEditMode = useRecoilComponentValue( isPageLayoutInEditModeComponentState, ); + const { hasAccess, restriction } = useWidgetPermissions(widget); + return ( - handleEditWidget({ widgetId: widget.id, widgetType: widget.type }) + handleEditWidget({ + widgetId: widget.id, + widgetType: widget.type, + }) } onRemove={() => deletePageLayoutWidget(widget.id)} /> 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 7ce50f79cd..a00316119f 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 @@ -1,18 +1,142 @@ -import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper'; -import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget'; -import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; +import { + type ApolloClient, + type NormalizedCacheObject, + useApolloClient, +} from '@apollo/client'; +import { type MockedResponse } from '@apollo/client/testing'; import { type Meta, type StoryObj } from '@storybook/react'; -import { ComponentDecorator } from 'twenty-ui/testing'; -import { GraphType } from '~/generated-metadata/graphql'; +import { type MutableSnapshot } from 'recoil'; +import { MemoryRouter } from 'react-router-dom'; + +import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { PageLayoutTestWrapper } from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper'; +import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; +import { generateGroupByQuery } from '@/page-layout/widgets/graph/utils/generateGroupByQuery'; +import { + GraphOrderBy, + GraphType, + WidgetType, +} from '~/generated-metadata/graphql'; +import { + AxisNameDisplay, + ExtendedAggregateOperations, + type PageLayoutWidget, +} from '~/generated/graphql'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; +import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow'; +import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow'; + +const companyObjectMetadataItem = getMockObjectMetadataItemOrThrow( + CoreObjectNameSingular.Company, +); +const idField = getMockFieldMetadataItemOrThrow({ + objectMetadataItem: companyObjectMetadataItem, + fieldName: 'id', +}); +const createdAtField = getMockFieldMetadataItemOrThrow({ + objectMetadataItem: companyObjectMetadataItem, + fieldName: 'createdAt', +}); + +const barChartGroupByQuery = generateGroupByQuery({ + objectMetadataItem: companyObjectMetadataItem, + aggregateOperations: ['totalCount'], +}); + +const graphqlMocks: MockedResponse[] = [ + { + request: { + query: barChartGroupByQuery, + variables: { + groupBy: [ + { + createdAt: { + granularity: 'DAY', + }, + }, + ], + }, + }, + result: { + data: { + companiesGroupBy: [ + { + groupByDimensionValues: ['2024-01-15T00:00:00.000Z'], + totalCount: 12, + }, + { + groupByDimensionValues: ['2024-02-15T00:00:00.000Z'], + totalCount: 18, + }, + { + groupByDimensionValues: ['2024-03-15T00:00:00.000Z'], + totalCount: 25, + }, + { + groupByDimensionValues: ['2024-04-15T00:00:00.000Z'], + totalCount: 15, + }, + { + groupByDimensionValues: ['2024-05-15T00:00:00.000Z'], + totalCount: 22, + }, + { + groupByDimensionValues: ['2024-06-15T00:00:00.000Z'], + totalCount: 30, + }, + ], + }, + }, + }, +]; + +const CoreClientProviderWrapper = ({ + children, +}: { + children: React.ReactNode; +}) => { + const apolloClient = useApolloClient() as ApolloClient; + + return ( + + {children} + + ); +}; + +const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: graphqlMocks, +}); const meta: Meta = { title: 'Modules/PageLayout/Widgets/WidgetRenderer', component: WidgetRenderer, decorators: [ - (Story, context) => ( - {Story(context)} - ), - ComponentDecorator, + (Story) => { + const initializeState = (snapshot: MutableSnapshot) => { + snapshot.set( + objectMetadataItemsState, + generatedMockObjectMetadataItems, + ); + snapshot.set(isAppWaitingForFreshObjectMetadataState, false); + }; + + return ( + + + + + + + + + + ); + }, ], parameters: { layout: 'centered', @@ -30,18 +154,31 @@ type Story = StoryObj; export const WithNumberChart: Story = { args: { - widget: createDefaultGraphWidget({ + widget: { + __typename: 'PageLayoutWidget', id: 'widget-1', pageLayoutTabId: 'tab-overview', + type: WidgetType.GRAPH, title: 'Sales Pipeline', - graphType: GraphType.NUMBER, + objectMetadataId: companyObjectMetadataItem.id, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 2, columnSpan: 3, }, - }), + configuration: { + __typename: 'NumberChartConfiguration', + graphType: GraphType.NUMBER, + aggregateOperation: ExtendedAggregateOperations.COUNT, + aggregateFieldMetadataId: idField.id, + displayDataLabel: true, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + deletedAt: null, + } as PageLayoutWidget, }, render: (args) => (
@@ -52,18 +189,31 @@ export const WithNumberChart: Story = { export const WithGaugeChart: Story = { args: { - widget: createDefaultGraphWidget({ - id: 'widget-1', + widget: { + __typename: 'PageLayoutWidget', + id: 'widget-2', pageLayoutTabId: 'tab-overview', + type: WidgetType.GRAPH, title: 'Conversion Rate', - graphType: GraphType.GAUGE, + objectMetadataId: companyObjectMetadataItem.id, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 5, columnSpan: 3, }, - }), + configuration: { + __typename: 'GaugeChartConfiguration', + graphType: GraphType.GAUGE, + aggregateOperation: ExtendedAggregateOperations.COUNT, + aggregateFieldMetadataId: idField.id, + displayDataLabel: false, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + deletedAt: null, + } as PageLayoutWidget, }, render: (args) => (
@@ -72,20 +222,36 @@ export const WithGaugeChart: Story = { ), }; -export const WithPieChart: Story = { +export const WithBarChart: Story = { args: { - widget: createDefaultGraphWidget({ - id: 'widget-1', + widget: { + __typename: 'PageLayoutWidget', + id: 'widget-3', pageLayoutTabId: 'tab-overview', - title: 'Lead Distribution', - graphType: GraphType.PIE, + type: WidgetType.GRAPH, + title: 'Monthly Trends', + objectMetadataId: companyObjectMetadataItem.id, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 5, columnSpan: 3, }, - }), + configuration: { + __typename: 'BarChartConfiguration', + graphType: GraphType.BAR, + aggregateOperation: ExtendedAggregateOperations.COUNT, + aggregateFieldMetadataId: idField.id, + groupByFieldMetadataIdX: createdAtField.id, + orderByX: GraphOrderBy.FIELD_ASC, + axisNameDisplay: AxisNameDisplay.BOTH, + displayDataLabel: false, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + deletedAt: null, + } as PageLayoutWidget, }, render: (args) => (
@@ -96,18 +262,31 @@ export const WithPieChart: Story = { export const SmallWidget: Story = { args: { - widget: createDefaultGraphWidget({ - id: 'widget-1', + widget: { + __typename: 'PageLayoutWidget', + id: 'widget-4', pageLayoutTabId: 'tab-overview', + type: WidgetType.GRAPH, title: 'Small Widget (2x2 grid)', - graphType: GraphType.NUMBER, + objectMetadataId: companyObjectMetadataItem.id, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 2, columnSpan: 2, }, - }), + configuration: { + __typename: 'NumberChartConfiguration', + graphType: GraphType.NUMBER, + aggregateOperation: ExtendedAggregateOperations.COUNT, + aggregateFieldMetadataId: idField.id, + displayDataLabel: true, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + deletedAt: null, + } as PageLayoutWidget, }, parameters: { docs: { @@ -125,18 +304,34 @@ export const SmallWidget: Story = { export const MediumWidget: Story = { args: { - widget: createDefaultGraphWidget({ - id: 'widget-1', + widget: { + __typename: 'PageLayoutWidget', + id: 'widget-5', pageLayoutTabId: 'tab-overview', + type: WidgetType.GRAPH, title: 'Medium Widget (4x3 grid)', - graphType: GraphType.GAUGE, + objectMetadataId: companyObjectMetadataItem.id, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 3, columnSpan: 4, }, - }), + configuration: { + __typename: 'BarChartConfiguration', + graphType: GraphType.BAR, + aggregateOperation: ExtendedAggregateOperations.COUNT, + aggregateFieldMetadataId: idField.id, + groupByFieldMetadataIdX: createdAtField.id, + orderByX: GraphOrderBy.FIELD_ASC, + axisNameDisplay: AxisNameDisplay.BOTH, + displayDataLabel: false, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + deletedAt: null, + } as PageLayoutWidget, }, parameters: { docs: { @@ -154,18 +349,34 @@ export const MediumWidget: Story = { export const LargeWidget: Story = { args: { - widget: createDefaultGraphWidget({ - id: 'widget-1', + widget: { + __typename: 'PageLayoutWidget', + id: 'widget-6', pageLayoutTabId: 'tab-overview', + type: WidgetType.GRAPH, title: 'Large Widget (6x4 grid)', - graphType: GraphType.PIE, + objectMetadataId: companyObjectMetadataItem.id, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 4, columnSpan: 6, }, - }), + configuration: { + __typename: 'BarChartConfiguration', + graphType: GraphType.BAR, + aggregateOperation: ExtendedAggregateOperations.COUNT, + aggregateFieldMetadataId: idField.id, + groupByFieldMetadataIdX: createdAtField.id, + orderByX: GraphOrderBy.FIELD_ASC, + axisNameDisplay: AxisNameDisplay.BOTH, + displayDataLabel: false, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + deletedAt: null, + } as PageLayoutWidget, }, parameters: { docs: { @@ -183,18 +394,31 @@ export const LargeWidget: Story = { export const WideWidget: Story = { args: { - widget: createDefaultGraphWidget({ - id: 'widget-1', + widget: { + __typename: 'PageLayoutWidget', + id: 'widget-7', pageLayoutTabId: 'tab-overview', + type: WidgetType.GRAPH, title: 'Wide Widget (8x2 grid)', - graphType: GraphType.NUMBER, + objectMetadataId: companyObjectMetadataItem.id, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 2, columnSpan: 8, }, - }), + configuration: { + __typename: 'NumberChartConfiguration', + graphType: GraphType.NUMBER, + aggregateOperation: ExtendedAggregateOperations.COUNT, + aggregateFieldMetadataId: idField.id, + displayDataLabel: true, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + deletedAt: null, + } as PageLayoutWidget, }, parameters: { docs: { @@ -212,18 +436,34 @@ export const WideWidget: Story = { export const TallWidget: Story = { args: { - widget: createDefaultGraphWidget({ - id: 'widget-1', + widget: { + __typename: 'PageLayoutWidget', + id: 'widget-8', pageLayoutTabId: 'tab-overview', + type: WidgetType.GRAPH, title: 'Tall Widget (3x6 grid)', - graphType: GraphType.GAUGE, + objectMetadataId: companyObjectMetadataItem.id, gridPosition: { + __typename: 'GridPosition', row: 0, column: 0, rowSpan: 6, columnSpan: 3, }, - }), + configuration: { + __typename: 'BarChartConfiguration', + graphType: GraphType.BAR, + aggregateOperation: ExtendedAggregateOperations.COUNT, + aggregateFieldMetadataId: idField.id, + groupByFieldMetadataIdX: createdAtField.id, + orderByX: GraphOrderBy.FIELD_ASC, + axisNameDisplay: AxisNameDisplay.BOTH, + displayDataLabel: false, + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + deletedAt: null, + } as PageLayoutWidget, }, parameters: { docs: { 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 new file mode 100644 index 0000000000..2083ebb289 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx @@ -0,0 +1,133 @@ +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { getDefaultWidgetData } from '@/page-layout/utils/getDefaultWidgetData'; +import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay'; +import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; +import { GraphWidgetBarChartRenderer } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer'; +import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart'; +import { areChartConfigurationFieldsValidForQuery } from '@/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery'; +import { lazy, Suspense } from 'react'; +import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; + +const GraphWidgetLineChart = lazy(() => + import( + '@/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart' + ).then((module) => ({ + default: module.GraphWidgetLineChart, + })), +); + +const GraphWidgetPieChart = lazy(() => + import( + '@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart' + ).then((module) => ({ + default: module.GraphWidgetPieChart, + })), +); + +const GraphWidgetGaugeChart = lazy(() => + import( + '@/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart' + ).then((module) => ({ + default: module.GraphWidgetGaugeChart, + })), +); + +export type GraphWidgetProps = { + widget: PageLayoutWidget; + objectMetadataId: string; + graphType: GraphType; +}; + +export const GraphWidget = ({ + widget, + objectMetadataId, + graphType, +}: GraphWidgetProps) => { + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: objectMetadataId, + }); + + const hasValidConfiguration = areChartConfigurationFieldsValidForQuery( + widget.configuration, + objectMetadataItem, + ); + + if (!hasValidConfiguration) { + return ; + } + + const data: any = getDefaultWidgetData(graphType); + + if (!data) { + return null; + } + + switch (graphType) { + case GraphType.NUMBER: + return ( + + ); + + case GraphType.GAUGE: + return ( + }> + + + ); + + case GraphType.PIE: + return ( + }> + + + ); + + case GraphType.BAR: + return ; + + case GraphType.LINE: + return ( + }> + + + ); + + default: + return null; + } +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx index 6029fa0ba8..76816771cb 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx @@ -1,33 +1,7 @@ -import { getDefaultWidgetData } from '@/page-layout/utils/getDefaultWidgetData'; -import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; -import { GraphWidgetBarChartRenderer } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer'; -import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart'; -import { lazy, Suspense } from 'react'; -import { GraphType, type PageLayoutWidget } from '~/generated-metadata/graphql'; - -const GraphWidgetLineChart = lazy(() => - import( - '@/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart' - ).then((module) => ({ - default: module.GraphWidgetLineChart, - })), -); - -const GraphWidgetPieChart = lazy(() => - import( - '@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart' - ).then((module) => ({ - default: module.GraphWidgetPieChart, - })), -); - -const GraphWidgetGaugeChart = lazy(() => - import( - '@/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart' - ).then((module) => ({ - default: module.GraphWidgetGaugeChart, - })), -); +import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay'; +import { GraphWidget } from '@/page-layout/widgets/graph/components/GraphWidget'; +import { isDefined } from 'twenty-shared/utils'; +import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; type GraphWidgetRendererProps = { widget: PageLayoutWidget; @@ -35,91 +9,23 @@ type GraphWidgetRendererProps = { export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => { if (!widget.configuration || !('graphType' in widget.configuration)) { - throw new Error( - `Invalid configuration for widget ${widget.id}: missing graphType`, - ); + return ; } const graphType = widget.configuration.graphType; - if (!Object.values(GraphType).includes(graphType)) { - throw new Error( - `Unsupported graph type ${graphType} for widget ${widget.id}`, - ); + if ( + !Object.values(GraphType).includes(graphType) || + !isDefined(widget.objectMetadataId) + ) { + return ; } - const data: any = getDefaultWidgetData(graphType); - - if (!data) { - return null; - } - - switch (graphType) { - case GraphType.NUMBER: - return ( - - ); - - case GraphType.GAUGE: - return ( - }> - - - ); - - case GraphType.PIE: - return ( - }> - - - ); - - case GraphType.BAR: - return ; - - case GraphType.LINE: - return ( - }> - - - ); - - default: - return null; - } + return ( + + ); }; 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 new file mode 100644 index 0000000000..3168a80899 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts @@ -0,0 +1,70 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isDefined } from 'twenty-shared/utils'; +import { type PageLayoutWidget } from '~/generated/graphql'; + +const fieldExists = ( + fieldId: string | undefined | null, + objectMetadataItem: ObjectMetadataItem, +): boolean => { + if (!isDefined(fieldId)) { + return false; + } + + return objectMetadataItem.fields.some((field) => field.id === fieldId); +}; + +export const areChartConfigurationFieldsValidForQuery = ( + configuration: PageLayoutWidget['configuration'], + objectMetadataItem?: ObjectMetadataItem | null, +): boolean => { + if (!isDefined(configuration)) { + return false; + } + + if (!isDefined(objectMetadataItem) || !isDefined(objectMetadataItem.fields)) { + return false; + } + + switch (configuration.__typename) { + case 'BarChartConfiguration': + case 'LineChartConfiguration': { + const hasRequiredXFields = + fieldExists( + configuration.aggregateFieldMetadataId, + objectMetadataItem, + ) && + fieldExists(configuration.groupByFieldMetadataIdX, objectMetadataItem); + + const hasValidYField = + !isDefined(configuration.groupByFieldMetadataIdY) || + fieldExists(configuration.groupByFieldMetadataIdY, objectMetadataItem); + + return hasRequiredXFields && hasValidYField; + } + + case 'PieChartConfiguration': + return ( + fieldExists( + configuration.aggregateFieldMetadataId, + objectMetadataItem, + ) && + fieldExists(configuration.groupByFieldMetadataId, objectMetadataItem) + ); + + case 'NumberChartConfiguration': + case 'GaugeChartConfiguration': + return fieldExists( + configuration.aggregateFieldMetadataId, + objectMetadataItem, + ); + + case 'IframeConfiguration': + return ( + typeof configuration.url === 'string' && + configuration.url.trim().length > 0 + ); + + default: + return false; + } +}; 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 824758b9c0..ee5fa13c26 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 @@ -103,7 +103,7 @@ export const transformGroupByDataToBarChartData = ({ ? `${getAggregateOperationLabel(configuration.aggregateOperation)} of ${aggregateField.label}` : undefined; - const showDataLabels = configuration.displayDataLabel; + const showDataLabels = configuration.displayDataLabel ?? false; const baseResult = isDefined(groupByFieldY) ? transformTwoDimensionalGroupByToBarChartData({ diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout-tab.controller.ts b/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout-tab.controller.ts index 9d99f8ce2b..28ea96bb7c 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout-tab.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout-tab.controller.ts @@ -28,7 +28,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -@Controller('rest/metadata/page-layout-tabs') +@Controller('rest/metadata/pageLayoutTabs') @UseGuards(WorkspaceAuthGuard) @UseFilters(PageLayoutTabRestApiExceptionFilter) export class PageLayoutTabController { diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout-widget.controller.ts b/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout-widget.controller.ts index fed9074902..00192b7ded 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout-widget.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout-widget.controller.ts @@ -28,7 +28,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -@Controller('rest/metadata/page-layout-widgets') +@Controller('rest/metadata/pageLayoutWidgets') @UseGuards(WorkspaceAuthGuard) @UseFilters(PageLayoutWidgetRestApiExceptionFilter) export class PageLayoutWidgetController { diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout.controller.ts b/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout.controller.ts index 75108b25ee..1ed98e434d 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/controllers/page-layout.controller.ts @@ -23,7 +23,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -@Controller('rest/metadata/page-layouts') +@Controller('rest/metadata/pageLayouts') @UseGuards(WorkspaceAuthGuard) @UseFilters(PageLayoutRestApiExceptionFilter) export class PageLayoutController { 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 38db321764..d994a3d264 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 @@ -47,10 +47,10 @@ export class BarChartConfigurationDTO { @IsOptional() groupBySubFieldNameX?: string; - @Field(() => GraphOrderBy) + @Field(() => GraphOrderBy, { nullable: true }) @IsEnum(GraphOrderBy) - @IsNotEmpty() - orderByX: GraphOrderBy; + @IsOptional() + orderByX?: GraphOrderBy; @Field(() => UUIDScalarType, { nullable: true }) @IsUUID() @@ -72,15 +72,18 @@ export class BarChartConfigurationDTO { @IsOptional() omitNullValues?: boolean; - @Field(() => AxisNameDisplay) + @Field(() => AxisNameDisplay, { + nullable: true, + defaultValue: AxisNameDisplay.BOTH, + }) @IsEnum(AxisNameDisplay) - @IsNotEmpty() - axisNameDisplay: AxisNameDisplay; + @IsOptional() + axisNameDisplay?: AxisNameDisplay; - @Field(() => Boolean) + @Field(() => Boolean, { nullable: true, defaultValue: false }) @IsBoolean() - @IsNotEmpty() - displayDataLabel: boolean; + @IsOptional() + displayDataLabel?: boolean; @Field(() => Number, { nullable: true }) @IsNumber() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts index 366dcb08e8..5189b38f3a 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/gauge-chart-configuration.dto.ts @@ -34,9 +34,10 @@ export class GaugeChartConfigurationDTO { @IsNotEmpty() aggregateOperation: ExtendedAggregateOperations; - @Field(() => Boolean) + @Field(() => Boolean, { nullable: true, defaultValue: false }) @IsBoolean() - displayDataLabel: boolean; + @IsOptional() + displayDataLabel?: boolean; @Field(() => String, { nullable: true }) @IsString() 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 8e4cc909c5..4dcc02f9be 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 @@ -47,10 +47,13 @@ export class LineChartConfigurationDTO { @IsOptional() groupBySubFieldNameX?: string; - @Field(() => GraphOrderBy) + @Field(() => GraphOrderBy, { + nullable: true, + defaultValue: GraphOrderBy.FIELD_ASC, + }) @IsEnum(GraphOrderBy) - @IsNotEmpty() - orderByX: GraphOrderBy; + @IsOptional() + orderByX?: GraphOrderBy; @Field(() => UUIDScalarType, { nullable: true }) @IsUUID() @@ -72,15 +75,18 @@ export class LineChartConfigurationDTO { @IsOptional() omitNullValues?: boolean; - @Field(() => AxisNameDisplay) + @Field(() => AxisNameDisplay, { + nullable: true, + defaultValue: AxisNameDisplay.BOTH, + }) @IsEnum(AxisNameDisplay) - @IsNotEmpty() - axisNameDisplay: AxisNameDisplay; + @IsOptional() + axisNameDisplay?: AxisNameDisplay; - @Field(() => Boolean) + @Field(() => Boolean, { nullable: true, defaultValue: false }) @IsBoolean() - @IsNotEmpty() - displayDataLabel: boolean; + @IsOptional() + displayDataLabel?: boolean; @Field(() => Number, { nullable: true }) @IsNumber() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts index de54ea03b6..a92b55afdf 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto.ts @@ -39,10 +39,10 @@ export class NumberChartConfigurationDTO { @IsOptional() label?: string; - @Field(() => Boolean) + @Field(() => Boolean, { nullable: true, defaultValue: false }) @IsBoolean() - @IsNotEmpty() - displayDataLabel: boolean; + @IsOptional() + displayDataLabel?: boolean; @Field(() => String, { nullable: true }) @IsString() @@ -54,11 +54,6 @@ export class NumberChartConfigurationDTO { @IsOptional() description?: string; - @Field(() => String, { nullable: true }) - @IsString() - @IsOptional() - color?: string; - @Field(() => GraphQLJSON, { nullable: true }) @IsObject() @IsOptional() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts index 5764869226..0359afe5fe 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto.ts @@ -45,15 +45,18 @@ export class PieChartConfigurationDTO { @IsOptional() groupBySubFieldName?: string; - @Field(() => GraphOrderBy) + @Field(() => GraphOrderBy, { + nullable: true, + defaultValue: GraphOrderBy.VALUE_DESC, + }) @IsEnum(GraphOrderBy) - @IsNotEmpty() - orderBy: GraphOrderBy; + @IsOptional() + orderBy?: GraphOrderBy; - @Field(() => Boolean) + @Field(() => Boolean, { nullable: true, defaultValue: false }) @IsBoolean() - @IsNotEmpty() - displayDataLabel: boolean; + @IsOptional() + displayDataLabel?: boolean; @Field(() => String, { nullable: true }) @IsString() 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 056a189853..e1e6b17ad2 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 @@ -6,6 +6,7 @@ import { IframeConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos import { LineChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto'; import { NumberChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/number-chart-configuration.dto'; import { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto'; +import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum'; export const WidgetConfiguration = createUnionType({ @@ -25,24 +26,34 @@ export const WidgetConfiguration = createUnionType({ ); } - switch (configuration.configurationType) { - case WidgetConfigurationType.IFRAME_CONFIG: - return IframeConfigurationDTO; - case WidgetConfigurationType.BAR_CHART_CONFIG: - return BarChartConfigurationDTO; - case WidgetConfigurationType.LINE_CHART_CONFIG: - return LineChartConfigurationDTO; - case WidgetConfigurationType.PIE_CHART_CONFIG: - return PieChartConfigurationDTO; - case WidgetConfigurationType.NUMBER_CHART_CONFIG: - return NumberChartConfigurationDTO; - case WidgetConfigurationType.GAUGE_CHART_CONFIG: - return GaugeChartConfigurationDTO; - default: - throw new Error( - `Unknown widget configuration type: ${configuration.configurationType}`, - ); + if ( + configuration.configurationType === WidgetConfigurationType.CHART_CONFIG + ) { + switch (configuration.graphType) { + case GraphType.BAR: + return BarChartConfigurationDTO; + case GraphType.LINE: + return LineChartConfigurationDTO; + case GraphType.PIE: + return PieChartConfigurationDTO; + case GraphType.NUMBER: + return NumberChartConfigurationDTO; + case GraphType.GAUGE: + return GaugeChartConfigurationDTO; + default: + throw new Error(`Unknown graph type: ${configuration.graphType}`); + } } + + if ( + configuration.configurationType === WidgetConfigurationType.IFRAME_CONFIG + ) { + return IframeConfigurationDTO; + } + + throw new Error( + `Unknown widget configuration type: ${configuration.configurationType}`, + ); }, }); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts b/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts index 49bdcbe460..bc8c8ec1e4 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/enums/widget-configuration-type.enum.ts @@ -1,8 +1,4 @@ export enum WidgetConfigurationType { - BAR_CHART_CONFIG = 'BAR_CHART_CONFIG', - LINE_CHART_CONFIG = 'LINE_CHART_CONFIG', - PIE_CHART_CONFIG = 'PIE_CHART_CONFIG', - NUMBER_CHART_CONFIG = 'NUMBER_CHART_CONFIG', - GAUGE_CHART_CONFIG = 'GAUGE_CHART_CONFIG', + CHART_CONFIG = 'CHART_CONFIG', IFRAME_CONFIG = 'IFRAME_CONFIG', } diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/resolvers/page-layout-widget.resolver.ts b/packages/twenty-server/src/engine/core-modules/page-layout/resolvers/page-layout-widget.resolver.ts index feb1cb9c62..2e86fa0ab2 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/resolvers/page-layout-widget.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/resolvers/page-layout-widget.resolver.ts @@ -1,11 +1,20 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { + Args, + Mutation, + Parent, + Query, + ResolveField, + Resolver, +} from '@nestjs/graphql'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { CreatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/create-page-layout-widget.input'; import { UpdatePageLayoutWidgetInput } from 'src/engine/core-modules/page-layout/dtos/inputs/update-page-layout-widget.input'; import { PageLayoutWidgetDTO } from 'src/engine/core-modules/page-layout/dtos/page-layout-widget.dto'; +import { WidgetConfiguration } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; import { PageLayoutWidgetService } from 'src/engine/core-modules/page-layout/services/page-layout-widget.service'; +import { injectWidgetConfigurationDiscriminator } from 'src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util'; import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/core-modules/page-layout/utils/page-layout-graphql-api-exception.filter'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; @@ -79,4 +88,12 @@ export class PageLayoutWidgetResolver { ): Promise { return this.pageLayoutWidgetService.restore(id, workspace.id); } + + @ResolveField(() => WidgetConfiguration, { nullable: true }) + configuration(@Parent() widget: PageLayoutWidgetDTO) { + return injectWidgetConfigurationDiscriminator( + widget.type, + widget.configuration, + ); + } } 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 46b537f827..7fa3be1e78 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 @@ -18,7 +18,6 @@ import { TEST_PIE_CHART_CONFIG_MINIMAL, } from 'test/integration/constants/widget-configuration-test-data.constants'; -import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum'; import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; import { validateAndTransformWidgetConfiguration } from 'src/engine/core-modules/page-layout/utils/validate-and-transform-widget-configuration.util'; @@ -31,9 +30,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_IFRAME_CONFIG); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.IFRAME_CONFIG, - ); }); it('should throw error for invalid URL', () => { @@ -73,9 +69,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.NUMBER_CHART_CONFIG, - ); }); it('should validate minimal number graph configuration', () => { @@ -85,18 +78,15 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_NUMBER_CHART_CONFIG_MINIMAL); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.NUMBER_CHART_CONFIG, - ); }); - it('should throw error for missing required fields', () => { + it('should throw error for partial number graph configuration with missing required fields', () => { expect(() => validateAndTransformWidgetConfiguration( WidgetType.GRAPH, INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS, ), - ).toThrow(/aggregateFieldMetadataId should not be empty/); + ).toThrow(/aggregateFieldMetadataId.*aggregateOperation/); }); it('should throw error for invalid UUID', () => { @@ -117,9 +107,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_BAR_CHART_CONFIG); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.BAR_CHART_CONFIG, - ); }); it('should validate minimal bar graph configuration', () => { @@ -129,18 +116,15 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_BAR_CHART_CONFIG_MINIMAL); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.BAR_CHART_CONFIG, - ); }); - it('should throw error for missing group by field', () => { + it('should throw error for partial bar graph configuration with missing required fields', () => { expect(() => validateAndTransformWidgetConfiguration( WidgetType.GRAPH, INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY, ), - ).toThrow(/groupByFieldMetadataIdX should not be empty/); + ).toThrow(/groupByFieldMetadataIdX/); }); }); @@ -152,9 +136,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_LINE_CHART_CONFIG); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.LINE_CHART_CONFIG, - ); }); it('should validate minimal line graph configuration', () => { @@ -164,9 +145,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_LINE_CHART_CONFIG_MINIMAL); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.LINE_CHART_CONFIG, - ); }); }); @@ -178,9 +156,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_PIE_CHART_CONFIG); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.PIE_CHART_CONFIG, - ); }); it('should validate minimal pie graph configuration', () => { @@ -190,9 +165,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_PIE_CHART_CONFIG_MINIMAL); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.PIE_CHART_CONFIG, - ); }); }); @@ -204,9 +176,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_GAUGE_CHART_CONFIG); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.GAUGE_CHART_CONFIG, - ); }); it('should validate minimal gauge graph configuration', () => { @@ -216,9 +185,6 @@ describe('validateAndTransformWidgetConfiguration', () => { ); expect(result).toMatchObject(TEST_GAUGE_CHART_CONFIG_MINIMAL); - expect((result as any)?.configurationType).toBe( - WidgetConfigurationType.GAUGE_CHART_CONFIG, - ); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util.ts b/packages/twenty-server/src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util.ts new file mode 100644 index 0000000000..d6ee0f2712 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/page-layout/utils/inject-widget-configuration-discriminator.util.ts @@ -0,0 +1,32 @@ +import { type WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; +import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum'; +import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; + +type ConfigurationWithDiscriminator = WidgetConfigurationInterface & { + configurationType: WidgetConfigurationType; +}; + +export const injectWidgetConfigurationDiscriminator = ( + widgetType: WidgetType, + configuration: WidgetConfigurationInterface | null, +): ConfigurationWithDiscriminator | null => { + if (!configuration) { + return null; + } + + if (widgetType === WidgetType.IFRAME) { + return { + ...configuration, + configurationType: WidgetConfigurationType.IFRAME_CONFIG, + } as ConfigurationWithDiscriminator; + } + + if (widgetType === WidgetType.GRAPH && 'graphType' in configuration) { + return { + ...configuration, + configurationType: WidgetConfigurationType.CHART_CONFIG, + } as ConfigurationWithDiscriminator; + } + + return configuration as ConfigurationWithDiscriminator; +}; 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 6f1dc9ec61..c9c6b0d84e 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 @@ -9,7 +9,6 @@ import { NumberChartConfigurationDTO } from 'src/engine/core-modules/page-layout import { PieChartConfigurationDTO } from 'src/engine/core-modules/page-layout/dtos/pie-chart-configuration.dto'; import { type WidgetConfigurationInterface } from 'src/engine/core-modules/page-layout/dtos/widget-configuration.interface'; import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.enum'; -import { WidgetConfigurationType } from 'src/engine/core-modules/page-layout/enums/widget-configuration-type.enum'; import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum'; const formatValidationErrors = (errors: ValidationError[]): string => { @@ -37,16 +36,16 @@ const validateGraphConfiguration = ( case GraphType.BAR: { const instance = plainToInstance(BarChartConfigurationDTO, configuration); - const errors = validateSync(instance); + const errors = validateSync(instance, { + whitelist: true, + forbidUnknownValues: true, + }); if (errors.length > 0) { throw errors; } - return { - ...instance, - configurationType: WidgetConfigurationType.BAR_CHART_CONFIG, - } as WidgetConfigurationInterface; + return instance; } case GraphType.LINE: { const instance = plainToInstance( @@ -54,30 +53,30 @@ const validateGraphConfiguration = ( configuration, ); - const errors = validateSync(instance); + const errors = validateSync(instance, { + whitelist: true, + forbidUnknownValues: true, + }); if (errors.length > 0) { throw errors; } - return { - ...instance, - configurationType: WidgetConfigurationType.LINE_CHART_CONFIG, - } as WidgetConfigurationInterface; + return instance; } case GraphType.PIE: { const instance = plainToInstance(PieChartConfigurationDTO, configuration); - const errors = validateSync(instance); + const errors = validateSync(instance, { + whitelist: true, + forbidUnknownValues: true, + }); if (errors.length > 0) { throw errors; } - return { - ...instance, - configurationType: WidgetConfigurationType.PIE_CHART_CONFIG, - } as WidgetConfigurationInterface; + return instance; } case GraphType.NUMBER: { const instance = plainToInstance( @@ -85,16 +84,16 @@ const validateGraphConfiguration = ( configuration, ); - const errors = validateSync(instance); + const errors = validateSync(instance, { + whitelist: true, + forbidUnknownValues: true, + }); if (errors.length > 0) { throw errors; } - return { - ...instance, - configurationType: WidgetConfigurationType.NUMBER_CHART_CONFIG, - } as WidgetConfigurationInterface; + return instance; } case GraphType.GAUGE: { const instance = plainToInstance( @@ -102,16 +101,16 @@ const validateGraphConfiguration = ( configuration, ); - const errors = validateSync(instance); + const errors = validateSync(instance, { + whitelist: true, + forbidUnknownValues: true, + }); if (errors.length > 0) { throw errors; } - return { - ...instance, - configurationType: WidgetConfigurationType.GAUGE_CHART_CONFIG, - } as WidgetConfigurationInterface; + return instance; } default: return null; @@ -123,16 +122,16 @@ const validateIframeConfiguration = ( ): WidgetConfigurationInterface | null => { const instance = plainToInstance(IframeConfigurationDTO, configuration); - const errors = validateSync(instance); + const errors = validateSync(instance, { + whitelist: true, + forbidUnknownValues: true, + }); if (errors.length > 0) { throw errors; } - return { - ...instance, - configurationType: WidgetConfigurationType.IFRAME_CONFIG, - } as WidgetConfigurationInterface; + return instance; }; export const validateAndTransformWidgetConfiguration = ( 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 955327aa11..0be50ba5e7 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 @@ -73,7 +73,6 @@ export const PAGE_LAYOUT_WIDGET_CONFIGURATION_FIELDS = ` aggregateOperation description filter - color format label } 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 8b9a09e34d..0a34d7cab8 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 @@ -22,7 +22,6 @@ export const TEST_NUMBER_CHART_CONFIG = { aggregateOperation: AggregateOperations.COUNT, label: 'Total Records', description: 'Count of all records', - color: 'blue', format: '0,0', displayDataLabel: true, }; diff --git a/packages/twenty-server/test/integration/rest/suites/page-layout-tab.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/page-layout-tab.integration-spec.ts index b3e724b3af..b7f0d01962 100644 --- a/packages/twenty-server/test/integration/rest/suites/page-layout-tab.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/page-layout-tab.integration-spec.ts @@ -78,7 +78,7 @@ describe('Page Layout Tab REST API', () => { await cleanupPageLayoutTabRecords(); }); - describe('GET /rest/metadata/page-layout-tabs', () => { + describe('GET /rest/metadata/pageLayoutTabs', () => { it('should return page layout tabs filtered by pageLayoutId', async () => { const input1 = { title: 'Tab 1', @@ -96,7 +96,7 @@ describe('Page Layout Tab REST API', () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-tabs?pageLayoutId=${testPageLayoutId}`, + path: `/metadata/pageLayoutTabs?pageLayoutId=${testPageLayoutId}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -111,7 +111,7 @@ describe('Page Layout Tab REST API', () => { it('should return empty array when no page layout tabs match pageLayoutId', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-tabs?pageLayoutId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, + path: `/metadata/pageLayoutTabs?pageLayoutId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -123,7 +123,7 @@ describe('Page Layout Tab REST API', () => { it('should return error when pageLayoutId is missing', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: '/metadata/page-layout-tabs', + path: '/metadata/pageLayoutTabs', bearer: API_KEY_ACCESS_TOKEN, }); @@ -137,7 +137,7 @@ describe('Page Layout Tab REST API', () => { }); }); - describe('POST /rest/metadata/page-layout-tabs', () => { + describe('POST /rest/metadata/pageLayoutTabs', () => { it('should create a new page layout tab with all properties', async () => { const input = { title: 'Test Tab', @@ -180,7 +180,7 @@ describe('Page Layout Tab REST API', () => { const response = await makeRestAPIRequest({ method: 'post', - path: '/metadata/page-layout-tabs', + path: '/metadata/pageLayoutTabs', body: pageLayoutTabData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -202,7 +202,7 @@ describe('Page Layout Tab REST API', () => { const response = await makeRestAPIRequest({ method: 'post', - path: '/metadata/page-layout-tabs', + path: '/metadata/pageLayoutTabs', body: pageLayoutTabData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -211,7 +211,7 @@ describe('Page Layout Tab REST API', () => { }); }); - describe('GET /rest/metadata/page-layout-tabs/:id', () => { + describe('GET /rest/metadata/pageLayoutTabs/:id', () => { it('should return a page layout tab by id', async () => { const input = { title: 'Tab', @@ -222,7 +222,7 @@ describe('Page Layout Tab REST API', () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`, + path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -238,7 +238,7 @@ describe('Page Layout Tab REST API', () => { it('should return error for non-existent page layout tab', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-tabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, + path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -253,7 +253,7 @@ describe('Page Layout Tab REST API', () => { }); }); - describe('PATCH /rest/metadata/page-layout-tabs/:id', () => { + describe('PATCH /rest/metadata/pageLayoutTabs/:id', () => { it('should update an existing page layout tab', async () => { const input = { title: 'Test Tab for Update', @@ -269,7 +269,7 @@ describe('Page Layout Tab REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`, + path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -299,7 +299,7 @@ describe('Page Layout Tab REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`, + path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -323,7 +323,7 @@ describe('Page Layout Tab REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layout-tabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, + path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -339,7 +339,7 @@ describe('Page Layout Tab REST API', () => { }); }); - describe('DELETE /rest/metadata/page-layout-tabs/:id', () => { + describe('DELETE /rest/metadata/pageLayoutTabs/:id', () => { it('should delete an existing page layout tab', async () => { const pageLayoutTabTitle = generateRecordName('Test Tab for Delete'); const pageLayoutTab = await createTestPageLayoutTabWithRestApi({ @@ -350,7 +350,7 @@ describe('Page Layout Tab REST API', () => { const deleteResponse = await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`, + path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -359,7 +359,7 @@ describe('Page Layout Tab REST API', () => { const getResponse = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-tabs/${pageLayoutTab.id}`, + path: `/metadata/pageLayoutTabs/${pageLayoutTab.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -376,7 +376,7 @@ describe('Page Layout Tab REST API', () => { it('should return error when deleting non-existent page layout tab', async () => { const response = await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layout-tabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, + path: `/metadata/pageLayoutTabs/${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); diff --git a/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts index 9524df11ab..f142e5065f 100644 --- a/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/page-layout-widget.integration-spec.ts @@ -93,7 +93,7 @@ describe('Page Layout Widget REST API', () => { await cleanupPageLayoutWidgetRecords(); }); - describe('GET /rest/metadata/page-layout-widgets', () => { + describe('GET /rest/metadata/pageLayoutWidgets', () => { it('should return page layout widgets filtered by pageLayoutTabId', async () => { const input1 = { title: 'Widget 1', @@ -123,7 +123,7 @@ describe('Page Layout Widget REST API', () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-widgets?pageLayoutTabId=${testPageLayoutTabId}`, + path: `/metadata/pageLayoutWidgets?pageLayoutTabId=${testPageLayoutTabId}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -140,7 +140,7 @@ describe('Page Layout Widget REST API', () => { it('should return empty array when no page layout widgets match pageLayoutTabId', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-widgets?pageLayoutTabId=${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, + path: `/metadata/pageLayoutWidgets?pageLayoutTabId=${TEST_NOT_EXISTING_PAGE_LAYOUT_TAB_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -152,7 +152,7 @@ describe('Page Layout Widget REST API', () => { it('should return error when pageLayoutTabId is missing', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: '/metadata/page-layout-widgets', + path: '/metadata/pageLayoutWidgets', bearer: API_KEY_ACCESS_TOKEN, }); @@ -166,7 +166,7 @@ describe('Page Layout Widget REST API', () => { }); }); - describe('POST /rest/metadata/page-layout-widgets', () => { + describe('POST /rest/metadata/pageLayoutWidgets', () => { it('should create a new page layout widget with all properties', async () => { const input = { title: 'Test Widget', @@ -232,7 +232,7 @@ describe('Page Layout Widget REST API', () => { const response = await makeRestAPIRequest({ method: 'post', - path: '/metadata/page-layout-widgets', + path: '/metadata/pageLayoutWidgets', body: pageLayoutWidgetData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -259,7 +259,7 @@ describe('Page Layout Widget REST API', () => { const response = await makeRestAPIRequest({ method: 'post', - path: '/metadata/page-layout-widgets', + path: '/metadata/pageLayoutWidgets', body: pageLayoutWidgetData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -275,7 +275,7 @@ describe('Page Layout Widget REST API', () => { const response = await makeRestAPIRequest({ method: 'post', - path: '/metadata/page-layout-widgets', + path: '/metadata/pageLayoutWidgets', body: pageLayoutWidgetData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -284,7 +284,7 @@ describe('Page Layout Widget REST API', () => { }); }); - describe('GET /rest/metadata/page-layout-widgets/:id', () => { + describe('GET /rest/metadata/pageLayoutWidgets/:id', () => { it('should return a page layout widget by id', async () => { const input = { title: 'Widget For Get', @@ -302,7 +302,7 @@ describe('Page Layout Widget REST API', () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`, + path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -318,7 +318,7 @@ describe('Page Layout Widget REST API', () => { it('should return error for non-existent page layout widget', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-widgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, + path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -333,7 +333,7 @@ describe('Page Layout Widget REST API', () => { }); }); - describe('PATCH /rest/metadata/page-layout-widgets/:id', () => { + describe('PATCH /rest/metadata/pageLayoutWidgets/:id', () => { it('should update an existing page layout widget', async () => { const input = { title: 'Test Widget for Update', @@ -363,7 +363,7 @@ describe('Page Layout Widget REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`, + path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -400,7 +400,7 @@ describe('Page Layout Widget REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`, + path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -425,7 +425,7 @@ describe('Page Layout Widget REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layout-widgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, + path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -441,7 +441,7 @@ describe('Page Layout Widget REST API', () => { }); }); - describe('DELETE /rest/metadata/page-layout-widgets/:id', () => { + describe('DELETE /rest/metadata/pageLayoutWidgets/:id', () => { it('should delete an existing page layout widget', async () => { const pageLayoutWidgetTitle = generateRecordName( 'Test Widget for Delete', @@ -460,7 +460,7 @@ describe('Page Layout Widget REST API', () => { const deleteResponse = await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`, + path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -469,7 +469,7 @@ describe('Page Layout Widget REST API', () => { const getResponse = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layout-widgets/${pageLayoutWidget.id}`, + path: `/metadata/pageLayoutWidgets/${pageLayoutWidget.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -486,7 +486,7 @@ describe('Page Layout Widget REST API', () => { it('should return error when deleting non-existent page layout widget', async () => { const response = await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layout-widgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, + path: `/metadata/pageLayoutWidgets/${TEST_NOT_EXISTING_PAGE_LAYOUT_WIDGET_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); diff --git a/packages/twenty-server/test/integration/rest/suites/page-layout.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/page-layout.integration-spec.ts index 5d303d3a2f..98737e8f0b 100644 --- a/packages/twenty-server/test/integration/rest/suites/page-layout.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/page-layout.integration-spec.ts @@ -63,11 +63,11 @@ describe('Page Layout REST API', () => { await cleanupPageLayoutRecords(); }); - describe('GET /rest/metadata/page-layouts', () => { + describe('GET /rest/metadata/pageLayouts', () => { it('should return all page layouts for workspace', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: '/metadata/page-layouts', + path: '/metadata/pageLayouts', bearer: API_KEY_ACCESS_TOKEN, }); @@ -84,7 +84,7 @@ describe('Page Layout REST API', () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layouts?objectMetadataId=${testObjectMetadataId}`, + path: `/metadata/pageLayouts?objectMetadataId=${testObjectMetadataId}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -100,7 +100,7 @@ describe('Page Layout REST API', () => { it('should return empty array when no page layouts match objectMetadataId', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layouts?objectMetadataId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, + path: `/metadata/pageLayouts?objectMetadataId=${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -110,7 +110,7 @@ describe('Page Layout REST API', () => { }); }); - describe('POST /rest/metadata/page-layouts', () => { + describe('POST /rest/metadata/pageLayouts', () => { it('should create a new page layout with all properties', async () => { const pageLayoutName = generateRecordName('Dashboard Page Layout'); const pageLayout = await createTestPageLayoutWithRestApi({ @@ -143,7 +143,7 @@ describe('Page Layout REST API', () => { await deleteTestPageLayoutWithRestApi(pageLayout.id); }); - describe('GET /rest/metadata/page-layouts/:id', () => { + describe('GET /rest/metadata/pageLayouts/:id', () => { it('should return a page layout by id', async () => { const pageLayoutName = generateRecordName('Test Page Layout for Get'); const pageLayout = await createTestPageLayoutWithRestApi({ @@ -154,7 +154,7 @@ describe('Page Layout REST API', () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layouts/${pageLayout.id}`, + path: `/metadata/pageLayouts/${pageLayout.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -172,7 +172,7 @@ describe('Page Layout REST API', () => { it('should return {} for non-existent page layout', async () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, + path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -187,7 +187,7 @@ describe('Page Layout REST API', () => { }); }); - describe('PATCH /rest/metadata/page-layouts/:id', () => { + describe('PATCH /rest/metadata/pageLayouts/:id', () => { it('should update an existing page layout', async () => { const pageLayoutName = generateRecordName( 'Test Page Layout for Update', @@ -206,7 +206,7 @@ describe('Page Layout REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layouts/${pageLayout.id}`, + path: `/metadata/pageLayouts/${pageLayout.id}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -237,7 +237,7 @@ describe('Page Layout REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layouts/${pageLayout.id}`, + path: `/metadata/pageLayouts/${pageLayout.id}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -261,7 +261,7 @@ describe('Page Layout REST API', () => { const response = await makeRestAPIRequest({ method: 'patch', - path: `/metadata/page-layouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, + path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, body: updateData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -277,7 +277,7 @@ describe('Page Layout REST API', () => { }); }); - describe('DELETE /rest/metadata/page-layouts/:id', () => { + describe('DELETE /rest/metadata/pageLayouts/:id', () => { it('should delete an existing page layout', async () => { const pageLayoutName = generateRecordName( 'Test Page Layout for Delete', @@ -290,7 +290,7 @@ describe('Page Layout REST API', () => { const deleteResponse = await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layouts/${pageLayout.id}`, + path: `/metadata/pageLayouts/${pageLayout.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -299,7 +299,7 @@ describe('Page Layout REST API', () => { const getResponse = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layouts/${pageLayout.id}`, + path: `/metadata/pageLayouts/${pageLayout.id}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -316,7 +316,7 @@ describe('Page Layout REST API', () => { it('should return 404 error when deleting non-existent page layout', async () => { const response = await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, + path: `/metadata/pageLayouts/${TEST_NOT_EXISTING_PAGE_LAYOUT_ID}`, bearer: API_KEY_ACCESS_TOKEN, }); @@ -347,7 +347,7 @@ describe('Page Layout REST API', () => { const response = await makeRestAPIRequest({ method: 'get', - path: `/metadata/page-layouts?objectMetadataId=${testObjectMetadataId}`, + path: `/metadata/pageLayouts?objectMetadataId=${testObjectMetadataId}`, bearer: API_KEY_ACCESS_TOKEN, }); diff --git a/packages/twenty-server/test/integration/rest/utils/page-layout-rest-api.util.ts b/packages/twenty-server/test/integration/rest/utils/page-layout-rest-api.util.ts index 45c420bf7d..b50993210a 100644 --- a/packages/twenty-server/test/integration/rest/utils/page-layout-rest-api.util.ts +++ b/packages/twenty-server/test/integration/rest/utils/page-layout-rest-api.util.ts @@ -16,7 +16,7 @@ export const createTestPageLayoutWithRestApi = async ( const response = await makeRestAPIRequest({ method: 'post', - path: '/metadata/page-layouts', + path: '/metadata/pageLayouts', body: pageLayoutData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -35,7 +35,7 @@ export const deleteTestPageLayoutWithRestApi = async ( ): Promise => { await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layouts/${pageLayoutId}`, + path: `/metadata/pageLayouts/${pageLayoutId}`, bearer: API_KEY_ACCESS_TOKEN, }).catch(() => {}); }; diff --git a/packages/twenty-server/test/integration/rest/utils/page-layout-tab-rest-api.util.ts b/packages/twenty-server/test/integration/rest/utils/page-layout-tab-rest-api.util.ts index 7c2940661a..a08f325b5e 100644 --- a/packages/twenty-server/test/integration/rest/utils/page-layout-tab-rest-api.util.ts +++ b/packages/twenty-server/test/integration/rest/utils/page-layout-tab-rest-api.util.ts @@ -14,7 +14,7 @@ export const createTestPageLayoutTabWithRestApi = async ( const response = await makeRestAPIRequest({ method: 'post', - path: '/metadata/page-layout-tabs', + path: '/metadata/pageLayoutTabs', body: pageLayoutTabData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -33,7 +33,7 @@ export const deleteTestPageLayoutTabWithRestApi = async ( ): Promise => { await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layout-tabs/${pageLayoutTabId}`, + path: `/metadata/pageLayoutTabs/${pageLayoutTabId}`, bearer: API_KEY_ACCESS_TOKEN, }); }; diff --git a/packages/twenty-server/test/integration/rest/utils/page-layout-widget-rest-api.util.ts b/packages/twenty-server/test/integration/rest/utils/page-layout-widget-rest-api.util.ts index 2d2fa06dc3..e83077c234 100644 --- a/packages/twenty-server/test/integration/rest/utils/page-layout-widget-rest-api.util.ts +++ b/packages/twenty-server/test/integration/rest/utils/page-layout-widget-rest-api.util.ts @@ -21,7 +21,7 @@ export const createTestPageLayoutWidgetWithRestApi = async ( const response = await makeRestAPIRequest({ method: 'post', - path: '/metadata/page-layout-widgets', + path: '/metadata/pageLayoutWidgets', body: pageLayoutWidgetData, bearer: API_KEY_ACCESS_TOKEN, }); @@ -40,7 +40,7 @@ export const deleteTestPageLayoutWidgetWithRestApi = async ( ): Promise => { await makeRestAPIRequest({ method: 'delete', - path: `/metadata/page-layout-widgets/${pageLayoutWidgetId}`, + path: `/metadata/pageLayoutWidgets/${pageLayoutWidgetId}`, bearer: API_KEY_ACCESS_TOKEN, }); };