diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx index 7e3a90ffc8..586beb48bd 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx @@ -1,13 +1,44 @@ import { type Meta, type StoryObj } from '@storybook/react'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { GraphWidgetPieChart } from '@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart'; import { CatalogDecorator, ComponentDecorator } from 'twenty-ui/testing'; +import { + AggregateOperations, + GraphType, + type PieChartConfiguration, +} from '~/generated/graphql'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { RootDecorator } from '~/testing/decorators/RootDecorator'; +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 mockObjectMetadataItemId = companyObjectMetadataItem.id; +const mockConfiguration: PieChartConfiguration = { + aggregateFieldMetadataId: idField.id, + aggregateOperation: AggregateOperations.COUNT, + graphType: GraphType.PIE, + groupByFieldMetadataId: idField.id, +}; const meta: Meta = { title: 'Modules/PageLayout/Widgets/GraphWidgetPieChart', component: GraphWidgetPieChart, - decorators: [ComponentDecorator, I18nFrontDecorator], + decorators: [ + ComponentDecorator, + I18nFrontDecorator, + ObjectMetadataItemsDecorator, + RootDecorator, + ], parameters: { layout: 'centered', }, @@ -72,6 +103,8 @@ export const Default: Story = { decimals={args.decimals} showLegend={args.showLegend} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -93,6 +126,8 @@ export const WithCenterMetric: Story = { data={args.data} showCenterMetric={args.showCenterMetric} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -114,6 +149,8 @@ export const WithDataLabels: Story = { data={args.data} showDataLabels={args.showDataLabels} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -141,6 +178,8 @@ export const Revenue: Story = { decimals={args.decimals} showLegend={args.showLegend} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -167,6 +206,8 @@ export const TaskStatus: Story = { decimals={args.decimals} showLegend={args.showLegend} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -192,6 +233,8 @@ export const TwoSlices: Story = { decimals={args.decimals} showLegend={args.showLegend} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -222,6 +265,8 @@ export const ManySlices: Story = { decimals={args.decimals} showLegend={args.showLegend} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -248,6 +293,8 @@ export const WithoutLegend: Story = { decimals={args.decimals} showLegend={args.showLegend} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -275,6 +322,8 @@ export const MarketShare: Story = { decimals={args.decimals} showLegend={args.showLegend} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), @@ -302,13 +351,20 @@ export const Storage: Story = { decimals={args.decimals} showLegend={args.showLegend} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), }; export const Catalog: Story = { - decorators: [CatalogDecorator, I18nFrontDecorator], + decorators: [ + CatalogDecorator, + I18nFrontDecorator, + ObjectMetadataItemsDecorator, + RootDecorator, + ], parameters: { catalog: { dimensions: [ @@ -355,6 +411,8 @@ export const Catalog: Story = { displayType="percentage" showLegend={true} id={args.id} + objectMetadataItemId={mockObjectMetadataItemId} + configuration={mockConfiguration} /> ), diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx index 3feaecf55b..993ce75500 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx @@ -18,11 +18,14 @@ import { } from '@nivo/pie'; import { useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { type PieChartConfiguration } from '~/generated/graphql'; type GraphWidgetPieChartProps = { data: PieChartDataItem[]; showLegend?: boolean; id: string; + objectMetadataItemId: string; + configuration: PieChartConfiguration; onSliceClick?: (datum: PieChartDataItem) => void; showDataLabels?: boolean; showCenterMetric?: boolean; @@ -59,6 +62,8 @@ export const GraphWidgetPieChart = ({ data, showLegend = true, id, + objectMetadataItemId, + configuration, displayType, decimals, prefix, @@ -158,8 +163,8 @@ export const GraphWidgetPieChart = ({ }} /> diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx index d2f8b6cf44..ef6925e637 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx @@ -91,6 +91,8 @@ export const GraphWidgetPieChartRenderer = ({ ` +const StyledCenterMetricContainer = styled(motion.div)` align-items: center; display: flex; flex-direction: column; justify-content: center; left: 50%; - opacity: ${({ show }) => (show ? 1 : 0)}; pointer-events: none; position: absolute; top: 50%; transform: translate(-50%, -50%); - transition: opacity ${({ theme }) => theme.animation.duration.fast}s - ease-in-out; `; const StyledValue = styled.span` @@ -39,18 +34,34 @@ const StyledLabel = styled.span` `; export const PieChartCenterMetric = ({ - data, - formatOptions, + objectMetadataItemId, + configuration, show, }: PieChartCenterMetricProps) => { - const total = data.reduce((sum, datum) => sum + datum.value, 0); + const theme = useTheme(); + + const { centerMetricValue, centerMetricLabel } = usePieChartCenterMetricData({ + objectMetadataItemId, + configuration, + skip: !show, + }); return ( - - {formatGraphValue(total, formatOptions)} - - Total - - + + {show && ( + + {centerMetricValue} + {centerMetricLabel} + + )} + ); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartCenterMetricData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartCenterMetricData.ts new file mode 100644 index 0000000000..d8b215e9ba --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartCenterMetricData.ts @@ -0,0 +1,135 @@ +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; +import { transformAggregateRawValueIntoAggregateDisplayValue } from '@/object-record/record-aggregate/utils/transformAggregateRawValueIntoAggregateDisplayValue'; +import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; +import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; +import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation'; +import { useGraphWidgetQueryCommon } from '@/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon'; +import { UserContext } from '@/users/contexts/UserContext'; +import { t } from '@lingui/core/macro'; +import { useContext, useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; +import { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from 'twenty-shared/constants'; +import { findById, isDefined } from 'twenty-shared/utils'; +import { type PieChartConfiguration } from '~/generated/graphql'; +import { dateLocaleState } from '~/localization/states/dateLocaleState'; + +type UsePieChartCenterMetricDataProps = { + objectMetadataItemId: string; + configuration: PieChartConfiguration; + skip: boolean; +}; + +type UsePieChartCenterMetricDataResult = { + centerMetricValue: string | number | undefined; + centerMetricLabel: string; +}; + +export const usePieChartCenterMetricData = ({ + objectMetadataItemId, + configuration, + skip, +}: UsePieChartCenterMetricDataProps): UsePieChartCenterMetricDataResult => { + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: objectMetadataItemId, + }); + + const { gqlOperationFilter, aggregateField } = useGraphWidgetQueryCommon({ + objectMetadataItemId, + configuration, + }); + + const centerMetricAggregateOperation = + configuration.aggregateOperation as AggregateOperations; + + const extendedAggregateOperation = + convertAggregateOperationToExtendedAggregateOperation( + centerMetricAggregateOperation, + aggregateField.type, + ); + + const recordGqlFieldsAggregate = useMemo( + () => ({ + [aggregateField.name]: [extendedAggregateOperation], + }), + [aggregateField.name, extendedAggregateOperation], + ); + + const { data: centerMetricData } = useAggregateRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + recordGqlFieldsAggregate, + filter: gqlOperationFilter, + skip, + }); + + const { dateFormat, timeFormat, timeZone } = useContext(UserContext); + const dateLocale = useRecoilValue(dateLocaleState); + + const aggregateFieldMetadataItem = objectMetadataItem.readableFields.find( + findById(configuration.aggregateFieldMetadataId), + ); + + const centerMetricLabel = useMemo(() => { + const isCountOrSum = + centerMetricAggregateOperation === AggregateOperations.SUM || + centerMetricAggregateOperation === AggregateOperations.COUNT || + centerMetricAggregateOperation === AggregateOperations.COUNT_EMPTY || + centerMetricAggregateOperation === AggregateOperations.COUNT_NOT_EMPTY || + centerMetricAggregateOperation === + AggregateOperations.COUNT_UNIQUE_VALUES || + centerMetricAggregateOperation === AggregateOperations.COUNT_TRUE || + centerMetricAggregateOperation === AggregateOperations.COUNT_FALSE; + + if (isCountOrSum) { + return t`Total`; + } + + const isPercentage = + centerMetricAggregateOperation === AggregateOperations.PERCENTAGE_EMPTY || + centerMetricAggregateOperation === + AggregateOperations.PERCENTAGE_NOT_EMPTY; + + if (isPercentage) { + return t`Percentage`; + } + + return getAggregateOperationLabel(centerMetricAggregateOperation); + }, [centerMetricAggregateOperation]); + + const centerMetricValue = useMemo(() => { + if (!isDefined(aggregateFieldMetadataItem)) { + return centerMetricData?.[FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION]?.[ + AggregateOperations.COUNT + ]; + } + + const aggregateRawValue = + centerMetricData?.[aggregateFieldMetadataItem.name]?.[ + centerMetricAggregateOperation + ]; + + return transformAggregateRawValueIntoAggregateDisplayValue({ + aggregateFieldMetadataItem, + aggregateOperation: extendedAggregateOperation, + aggregateRawValue, + dateFormat, + localeCatalog: dateLocale.localeCatalog, + timeFormat, + timeZone, + }); + }, [ + aggregateFieldMetadataItem, + centerMetricData, + centerMetricAggregateOperation, + extendedAggregateOperation, + dateFormat, + dateLocale.localeCatalog, + timeFormat, + timeZone, + ]); + + return { + centerMetricValue, + centerMetricLabel, + }; +};