From c8fe805ea58c45c58b1f0e429e511a55e5e7f7f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Wed, 22 Oct 2025 10:20:08 +0200
Subject: [PATCH] Connect the number chart to the backend (#15229)
https://github.com/user-attachments/assets/e8fecb5d-bc9b-425a-9c19-baca15f88b1c
---
.../widgets/graph/components/GraphWidget.tsx | 9 +--
.../GraphWidgetBarChartRenderer.tsx | 6 --
.../components/GraphWidgetNumberChart.tsx | 44 ++++++++-------
.../GraphWidgetNumberChartRenderer.tsx | 36 ++++++++++++
.../hooks/useGraphWidgetAggregateQuery.ts | 56 +++++++++++++++++++
.../graph/hooks/useGraphWidgetGroupByQuery.ts | 46 +++++----------
.../graph/hooks/useGraphWidgetQueryCommon.ts | 44 +++++++++++++++
7 files changed, 177 insertions(+), 64 deletions(-)
create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChartRenderer.tsx
create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetAggregateQuery.ts
create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx
index 48e65f1841..6ff1c44921 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx
@@ -3,7 +3,7 @@ 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 { GraphWidgetNumberChartRenderer } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChartRenderer';
import { areChartConfigurationFieldsValidForQuery } from '@/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery';
import { lazy, Suspense } from 'react';
import { GraphType, type PageLayoutWidget } from '~/generated/graphql';
@@ -64,12 +64,7 @@ export const GraphWidget = ({
switch (graphType) {
case GraphType.NUMBER:
- return (
-
- );
+ return ;
case GraphType.GAUGE:
return (
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx
index e113a0d456..280c06b268 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx
@@ -1,7 +1,6 @@
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData';
import { lazy, Suspense } from 'react';
-import { isDefined } from 'twenty-shared/utils';
import {
type BarChartConfiguration,
type PageLayoutWidget,
@@ -30,7 +29,6 @@ export const GraphWidgetBarChartRenderer = ({
showDataLabels,
layout,
loading,
- error,
} = useGraphBarChartWidgetData({
objectMetadataItemId: widget.objectMetadataId,
configuration: widget.configuration as BarChartConfiguration,
@@ -40,10 +38,6 @@ export const GraphWidgetBarChartRenderer = ({
return ;
}
- if (isDefined(error)) {
- return
Error: {error.message}
;
- }
-
const configuration = widget.configuration as BarChartConfiguration;
const groupMode =
configuration.groupMode === 'GROUPED' ? 'grouped' : 'stacked';
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart.tsx
index cf815ff0b1..96b0e5cbb0 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart.tsx
@@ -1,6 +1,7 @@
import { formatNumberChartTrend } from '@/page-layout/widgets/graph/graphWidgetNumberChart/utils/formatNumberChartTrend';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { isDefined } from 'twenty-shared/utils';
import {
H1Title,
H1TitleFontColor,
@@ -9,8 +10,8 @@ import {
} from 'twenty-ui/display';
type GraphWidgetNumberChartProps = {
- value: string;
- trendPercentage: number;
+ value: string | number;
+ trendPercentage?: number;
};
const StyledTrendPercentageValue = styled.span`
@@ -43,27 +44,32 @@ export const GraphWidgetNumberChart = ({
trendPercentage,
}: GraphWidgetNumberChartProps) => {
const theme = useTheme();
- const formattedPercentage = formatNumberChartTrend(trendPercentage);
+
+ const formattedPercentage = isDefined(trendPercentage)
+ ? formatNumberChartTrend(trendPercentage)
+ : undefined;
return (
-
-
- {formattedPercentage}%
-
- {trendPercentage >= 0 ? (
-
- ) : (
-
- )}
-
+ {isDefined(trendPercentage) && (
+
+
+ {formattedPercentage}%
+
+ {trendPercentage >= 0 ? (
+
+ ) : (
+
+ )}
+
+ )}
);
};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChartRenderer.tsx
new file mode 100644
index 0000000000..fd83142cb1
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChartRenderer.tsx
@@ -0,0 +1,36 @@
+import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
+import { useGraphWidgetAggregateQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetAggregateQuery';
+import { lazy, Suspense } from 'react';
+import {
+ type NumberChartConfiguration,
+ type PageLayoutWidget,
+} from '~/generated/graphql';
+
+const GraphWidgetNumberChart = lazy(() =>
+ import(
+ '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart'
+ ).then((module) => ({
+ default: module.GraphWidgetNumberChart,
+ })),
+);
+
+export const GraphWidgetNumberChartRenderer = ({
+ widget,
+}: {
+ widget: PageLayoutWidget;
+}) => {
+ const { value, loading } = useGraphWidgetAggregateQuery({
+ objectMetadataItemId: widget.objectMetadataId,
+ configuration: widget.configuration as NumberChartConfiguration,
+ });
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ }>
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetAggregateQuery.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetAggregateQuery.ts
new file mode 100644
index 0000000000..1f32eb7343
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetAggregateQuery.ts
@@ -0,0 +1,56 @@
+import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
+import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel';
+import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
+import { useGraphWidgetQueryCommon } from '@/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon';
+import { UserContext } from '@/users/contexts/UserContext';
+import { useContext } from 'react';
+import { useRecoilValue } from 'recoil';
+import { type NumberChartConfiguration } from '~/generated/graphql';
+import { dateLocaleState } from '~/localization/states/dateLocaleState';
+
+export const useGraphWidgetAggregateQuery = ({
+ objectMetadataItemId,
+ configuration,
+}: {
+ objectMetadataItemId: string;
+ configuration: NumberChartConfiguration;
+}) => {
+ const { objectMetadataItem, gqlOperationFilter, aggregateField } =
+ useGraphWidgetQueryCommon({
+ objectMetadataItemId,
+ configuration,
+ });
+
+ // TODO: Move this enum to shared
+ const aggregateOperation =
+ configuration.aggregateOperation as unknown as ExtendedAggregateOperations;
+
+ const { data, loading, error } = useAggregateRecords({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ recordGqlFieldsAggregate: {
+ [aggregateField.name]: [aggregateOperation],
+ },
+ filter: gqlOperationFilter,
+ });
+
+ const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
+ const dateLocale = useRecoilValue(dateLocaleState);
+
+ const { value, label } = computeAggregateValueAndLabel({
+ data,
+ objectMetadataItem,
+ fieldMetadataId: configuration.aggregateFieldMetadataId,
+ aggregateOperation,
+ dateFormat,
+ timeFormat,
+ timeZone,
+ localeCatalog: dateLocale.localeCatalog,
+ });
+
+ return {
+ value,
+ label,
+ loading,
+ error,
+ };
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts
index abad746a64..86c1edec53 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery.ts
@@ -1,14 +1,11 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
-import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
+import { useGraphWidgetQueryCommon } from '@/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon';
import { generateGroupByQuery } from '@/page-layout/widgets/graph/utils/generateGroupByQuery';
import { generateGroupByQueryVariablesFromBarChartConfiguration } from '@/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration';
import { useQuery } from '@apollo/client';
import { useMemo } from 'react';
-import {
- computeRecordGqlOperationFilter,
- isDefined,
-} from 'twenty-shared/utils';
+import { isDefined } from 'twenty-shared/utils';
import { type BarChartConfiguration } from '~/generated-metadata/graphql';
export const useGraphWidgetGroupByQuery = ({
@@ -18,9 +15,15 @@ export const useGraphWidgetGroupByQuery = ({
objectMetadataItemId: string;
configuration: BarChartConfiguration;
}) => {
- const { objectMetadataItem } = useObjectMetadataItemById({
- objectId: objectMetadataItemId,
- });
+ const { objectMetadataItem, aggregateField, gqlOperationFilter } =
+ useGraphWidgetQueryCommon({
+ objectMetadataItemId,
+ configuration,
+ });
+
+ if (!isDefined(aggregateField)) {
+ throw new Error('Aggregate field not found');
+ }
const availableAggregations = useMemo(
() =>
@@ -30,16 +33,6 @@ export const useGraphWidgetGroupByQuery = ({
[objectMetadataItem.readableFields],
);
- const aggregateFieldId = configuration.aggregateFieldMetadataId;
-
- const aggregateField = objectMetadataItem.readableFields.find(
- (field) => field.id === aggregateFieldId,
- );
-
- if (!isDefined(aggregateField)) {
- throw new Error('Aggregate field not found');
- }
-
const aggregateOperation =
availableAggregations[aggregateField.name]?.[
configuration.aggregateOperation
@@ -49,27 +42,16 @@ export const useGraphWidgetGroupByQuery = ({
throw new Error('Aggregate operation not found');
}
- const gqlOperationFilter = computeRecordGqlOperationFilter({
- fields: objectMetadataItem.fields,
- filterValueDependencies: {},
- recordFilters: configuration.filter?.recordFilters ?? [],
- recordFilterGroups: configuration.filter?.recordFilterGroups ?? [],
- });
-
- const filterQueryVariables = {
- filter: gqlOperationFilter,
- };
-
const groupByQueryVariables =
generateGroupByQueryVariablesFromBarChartConfiguration({
objectMetadataItem,
barChartConfiguration: configuration,
- aggregateOperation,
+ aggregateOperation: aggregateOperation,
});
const variables = {
...groupByQueryVariables,
- ...filterQueryVariables,
+ filter: gqlOperationFilter,
};
const query = generateGroupByQuery({
@@ -89,6 +71,6 @@ export const useGraphWidgetGroupByQuery = ({
loading,
error,
refetch,
- aggregateOperation,
+ aggregateOperation: aggregateOperation,
};
};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts
new file mode 100644
index 0000000000..fb67b54cd4
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts
@@ -0,0 +1,44 @@
+import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
+import {
+ computeRecordGqlOperationFilter,
+ isDefined,
+} from 'twenty-shared/utils';
+import {
+ type BarChartConfiguration,
+ type NumberChartConfiguration,
+} from '~/generated/graphql';
+
+export const useGraphWidgetQueryCommon = ({
+ objectMetadataItemId,
+ configuration,
+}: {
+ objectMetadataItemId: string;
+ configuration: BarChartConfiguration | NumberChartConfiguration;
+}) => {
+ const { objectMetadataItem } = useObjectMetadataItemById({
+ objectId: objectMetadataItemId,
+ });
+
+ const aggregateFieldId = configuration.aggregateFieldMetadataId;
+
+ const aggregateField = objectMetadataItem.readableFields.find(
+ (field) => field.id === aggregateFieldId,
+ );
+
+ if (!isDefined(aggregateField)) {
+ throw new Error('Aggregate field not found');
+ }
+
+ const gqlOperationFilter = computeRecordGqlOperationFilter({
+ fields: objectMetadataItem.fields,
+ filterValueDependencies: {},
+ recordFilters: configuration.filter?.recordFilters ?? [],
+ recordFilterGroups: configuration.filter?.recordFilterGroups ?? [],
+ });
+
+ return {
+ objectMetadataItem,
+ gqlOperationFilter,
+ aggregateField,
+ };
+};