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, + }; +};