From 793119b1171898da6c9e5f3695d2d727ea2f567c Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:24:36 +0530 Subject: [PATCH] part 1 of filter/sort drill down from charts (#15983) This Pr handles basic navigation on bar/slice click to the chart's source objects index view --- .../components/GraphWidgetFloatingTooltip.tsx | 6 +- .../graph/components/GraphWidgetTooltip.tsx | 20 ++-- .../GraphWidgetTooltip.stories.tsx | 6 +- .../components/GraphBarChartTooltip.tsx | 19 ++-- .../components/GraphWidgetBarChart.tsx | 8 +- .../GraphWidgetBarChartRenderer.tsx | 29 +++++- .../hooks/useGraphBarChartWidgetData.ts | 3 + .../utils/getBarChartTooltipData.ts | 14 +-- .../components/GraphWidgetGaugeChart.tsx | 8 +- .../hooks/useGaugeChartTooltip.ts | 3 - .../components/CustomCrosshairLayer.tsx | 46 +++++++-- .../components/GraphLineChartTooltip.tsx | 21 +++- .../components/GraphWidgetLineChart.tsx | 96 +++++++++---------- .../GraphWidgetLineChartRenderer.tsx | 26 ++++- .../hooks/__tests__/useLineChartData.test.ts | 44 +-------- .../hooks/useGraphLineChartWidgetData.ts | 3 + .../hooks/useLineChartData.ts | 8 -- .../graphWidgetLineTooltipComponentState.ts | 1 - .../components/GraphWidgetPieChart.tsx | 8 +- .../hooks/usePieChartTooltip.ts | 8 -- .../graph/types/GraphWidgetTooltipContent.ts | 1 - 21 files changed, 201 insertions(+), 177 deletions(-) diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx index fd7693fcb2..4e319fd481 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx @@ -14,7 +14,7 @@ type GraphWidgetFloatingTooltipProps = { items: GraphWidgetTooltipItem[]; indexLabel?: string; highlightedKey?: string; - linkTo?: string; + onGraphWidgetTooltipClick?: () => void; onMouseEnter?: () => void; onMouseLeave?: () => void; }; @@ -25,7 +25,7 @@ export const GraphWidgetFloatingTooltip = ({ items, indexLabel, highlightedKey, - linkTo, + onGraphWidgetTooltipClick, onMouseEnter, onMouseLeave, }: GraphWidgetFloatingTooltipProps) => { @@ -67,7 +67,7 @@ export const GraphWidgetFloatingTooltip = ({ items={items} indexLabel={indexLabel} highlightedKey={highlightedKey} - linkTo={linkTo} + onGraphWidgetTooltipClick={onGraphWidgetTooltipClick} /> diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetTooltip.tsx index d53e774ba7..eaf693baf0 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetTooltip.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetTooltip.tsx @@ -6,6 +6,7 @@ import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { IconArrowUpRight } from 'twenty-ui/display'; +import { isDefined } from 'twenty-shared/utils'; const StyledTooltip = styled.div` background: ${({ theme }) => theme.background.primary}; @@ -133,14 +134,14 @@ type GraphWidgetTooltipProps = { items: GraphWidgetTooltipItem[]; indexLabel?: string; highlightedKey?: string; - linkTo?: string; + onGraphWidgetTooltipClick?: () => void; }; export const GraphWidgetTooltip = ({ items, indexLabel, highlightedKey, - linkTo, + onGraphWidgetTooltipClick, }: GraphWidgetTooltipProps) => { const theme = useTheme(); @@ -149,11 +150,14 @@ export const GraphWidgetTooltip = ({ ); const shouldHighlight = filteredItems.length > 1; - const hasLink = isNonEmptyString(linkTo); + const hasGraphWidgetTooltipClick = isDefined(onGraphWidgetTooltipClick); return ( - + {indexLabel && ( {indexLabel} @@ -179,15 +183,11 @@ export const GraphWidgetTooltip = ({ - {hasLink && ( + {hasGraphWidgetTooltipClick && ( <> - { - window.location.href = String(linkTo); - }} - > + {t`Click to see data`} diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetTooltip.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetTooltip.stories.tsx index 8468f4bdfd..1bcff1b28f 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetTooltip.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetTooltip.stories.tsx @@ -41,7 +41,7 @@ export const WithClickHint: Story = { dotColor: 'green', }, ], - linkTo: '/sales/details', + onGraphWidgetTooltipClick: () => {}, }, }; @@ -63,7 +63,6 @@ export const MultipleItems: Story = { dotColor: 'purple', }, ], - linkTo: '/comparison/details', indexLabel: 'February 2', }, }; @@ -87,7 +86,6 @@ export const SuperLongText: Story = { dotColor: 'purple', }, ], - linkTo: '/financials/q4-2024', indexLabel: 'Q4 2024 Financial Year End (October - December) - North America Regional Performance Summary', }, @@ -217,7 +215,7 @@ export const ManyItemsWithScroll: Story = { dotColor: 'lime', }, ], - linkTo: '/annual-report/2024', + onGraphWidgetTooltipClick: () => {}, indexLabel: 'Annual Report 2024', }, }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip.tsx index d8920c91e4..e1662f451e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip.tsx @@ -6,16 +6,16 @@ import { getBarChartTooltipData } from '@/page-layout/widgets/graph/graphWidgetB import { getTooltipReferenceFromBarChartElementAnchor } from '@/page-layout/widgets/graph/utils/getTooltipReferenceFromBarChartElementAnchor'; import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { type ComputedDatum } from '@nivo/bar'; import { isDefined } from 'twenty-shared/utils'; type GraphBarChartTooltipProps = { containerId: string; enrichedKeys: BarChartEnrichedKey[]; - data: BarChartDataItem[]; - indexBy: string; formatOptions: GraphValueFormatOptions; enableGroupTooltip?: boolean; layout?: 'vertical' | 'horizontal'; + onBarClick?: (datum: ComputedDatum) => void; onMouseEnter?: () => void; onMouseLeave?: () => void; }; @@ -23,11 +23,10 @@ type GraphBarChartTooltipProps = { export const GraphBarChartTooltip = ({ containerId, enrichedKeys, - data, - indexBy, formatOptions, enableGroupTooltip = true, layout = 'vertical', + onBarClick, onMouseEnter, onMouseLeave, }: GraphBarChartTooltipProps) => { @@ -35,13 +34,19 @@ export const GraphBarChartTooltip = ({ graphWidgetBarTooltipComponentState, ); + const handleTooltipClick: (() => void) | undefined = isDefined(onBarClick) + ? () => { + if (isDefined(tooltipState)) { + onBarClick(tooltipState.datum); + } + } + : undefined; + const tooltipData = !isDefined(tooltipState) ? null : getBarChartTooltipData({ datum: tooltipState.datum, enrichedKeys, - data, - indexBy, formatOptions, enableGroupTooltip, layout, @@ -79,7 +84,7 @@ export const GraphBarChartTooltip = ({ items={tooltipData.tooltipItems} indexLabel={tooltipData.indexLabel} highlightedKey={tooltipData.hoveredKey} - linkTo={tooltipData.linkTo} + onGraphWidgetTooltipClick={handleTooltipClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} /> diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx index 4365e82e61..6093834fa5 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx @@ -54,6 +54,7 @@ type GraphWidgetBarChartProps = { rangeMin?: number; rangeMax?: number; omitNullValues?: boolean; + onBarClick?: (datum: ComputedDatum) => void; } & GraphValueFormatOptions; const StyledContainer = styled.div` @@ -87,6 +88,7 @@ export const GraphWidgetBarChart = ({ prefix, suffix, customFormatter, + onBarClick, }: GraphWidgetBarChartProps) => { const theme = useTheme(); const colorRegistry = createGraphColorRegistry(theme); @@ -119,7 +121,7 @@ export const GraphWidgetBarChart = ({ seriesLabels, }); - const hasClickableItems = data.some((item) => isDefined(item.to)); + const hasClickableItems = isDefined(onBarClick); const hideTooltip = () => setActiveBarTooltip(null); const debouncedHideTooltip = useDebouncedCallback(hideTooltip, 300); @@ -277,6 +279,7 @@ export const GraphWidgetBarChart = ({ tooltip={() => null} onMouseEnter={handleBarEnter} onMouseLeave={handleBarLeave} + onClick={onBarClick} theme={chartTheme} borderRadius={parseInt(theme.border.radius.sm)} /> @@ -285,11 +288,10 @@ export const GraphWidgetBarChart = ({ 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 1d66d20099..ef9ebf863f 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,10 +1,16 @@ import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect'; import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData'; +import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; import { getEffectiveGroupMode } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getEffectiveGroupMode'; import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey'; +import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector'; +import { type ComputedDatum } from '@nivo/bar'; import { lazy, Suspense } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import { AppPath } from 'twenty-shared/types'; +import { getAppPath, isDefined } from 'twenty-shared/utils'; import { type BarChartConfiguration, type PageLayoutWidget, @@ -34,11 +40,13 @@ export const GraphWidgetBarChartRenderer = ({ layout, loading, hasTooManyGroups, + objectMetadataItem, } = useGraphBarChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, configuration: widget.configuration as BarChartConfiguration, }); + const navigate = useNavigate(); const configuration = widget.configuration as BarChartConfiguration; const hasGroupByOnSecondaryAxis = isDefined( @@ -54,6 +62,24 @@ export const GraphWidgetBarChartRenderer = ({ configuration.omitNullValues, ); + const indexViewId = useRecoilValue( + coreIndexViewIdFromObjectMetadataItemFamilySelector({ + objectMetadataItemId: objectMetadataItem.id, + }), + ); + + const handleBarClick = (_datum: ComputedDatum) => { + return navigate( + getAppPath( + AppPath.RecordIndexPage, + { + objectNamePlural: objectMetadataItem.namePlural, + }, + isDefined(indexViewId) ? { viewId: indexViewId } : undefined, + ), + ); + }; + if (loading) { return ; } @@ -79,6 +105,7 @@ export const GraphWidgetBarChartRenderer = ({ rangeMin={configuration.rangeMin ?? undefined} rangeMax={configuration.rangeMax ?? undefined} omitNullValues={configuration.omitNullValues ?? false} + onBarClick={handleBarClick} /> ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts index 079e4a44aa..c0c1e7f8d8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts @@ -1,4 +1,5 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant'; import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem'; import { type BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout'; @@ -25,6 +26,7 @@ type UseGraphBarChartWidgetDataResult = { loading: boolean; error?: Error; hasTooManyGroups: boolean; + objectMetadataItem: ObjectMetadataItem; }; // TODO: Remove this once backend returns total group count @@ -63,6 +65,7 @@ export const useGraphBarChartWidgetData = ({ return { ...transformedData, + objectMetadataItem, loading, error, }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTooltipData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTooltipData.ts index 167011bb47..d8fb478dad 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTooltipData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTooltipData.ts @@ -5,14 +5,12 @@ import { formatGraphValue, type GraphValueFormatOptions, } from '@/page-layout/widgets/graph/utils/graphFormatters'; -import { type BarDatum, type ComputedDatum } from '@nivo/bar'; +import { type ComputedDatum } from '@nivo/bar'; import { isDefined } from 'twenty-shared/utils'; type GetBarChartTooltipDataParameters = { - datum: ComputedDatum; + datum: ComputedDatum; enrichedKeys: BarChartEnrichedKey[]; - data: BarChartDataItem[]; - indexBy: string; formatOptions: GraphValueFormatOptions; enableGroupTooltip?: boolean; layout?: 'vertical' | 'horizontal'; @@ -22,22 +20,15 @@ type BarChartTooltipData = { tooltipItems: GraphWidgetTooltipItem[]; indexLabel: string; hoveredKey: string | undefined; - linkTo: string | undefined; }; export const getBarChartTooltipData = ({ datum, enrichedKeys, - data, - indexBy, formatOptions, enableGroupTooltip = true, layout = 'vertical', }: GetBarChartTooltipDataParameters): BarChartTooltipData | null => { - const dataItem = data.find( - (dataRow) => dataRow[indexBy] === datum.indexValue, - ); - let keysToShow: BarChartEnrichedKey[]; if (enableGroupTooltip) { @@ -72,6 +63,5 @@ export const getBarChartTooltipData = ({ tooltipItems, indexLabel: String(datum.indexValue), hoveredKey: enableGroupTooltip ? String(datum.id) : undefined, - linkTo: isDefined(dataItem?.to) ? String(dataItem.to) : undefined, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart.tsx index fa7f403de4..5c8e97a984 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/components/GraphWidgetGaugeChart.tsx @@ -104,7 +104,6 @@ export const GraphWidgetGaugeChart = ({ label: data.label || t`Value`, colorScheme, formatOptions, - to: data.to, }); const formattedValue = formatGraphValue(data.value, formatOptions); @@ -120,12 +119,7 @@ export const GraphWidgetGaugeChart = ({ const renderTooltip = () => { const tooltipData = createTooltipData(); - return ( - - ); + return ; }; return ( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/hooks/useGaugeChartTooltip.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/hooks/useGaugeChartTooltip.ts index 7ff6728730..c219b45b85 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/hooks/useGaugeChartTooltip.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetGaugeChart/hooks/useGaugeChartTooltip.ts @@ -10,7 +10,6 @@ type UseGaugeChartTooltipProps = { label: string; colorScheme: GraphColorScheme; formatOptions: GraphValueFormatOptions; - to?: string; }; export const useGaugeChartTooltip = ({ @@ -19,7 +18,6 @@ export const useGaugeChartTooltip = ({ label, colorScheme, formatOptions, - to, }: UseGaugeChartTooltipProps) => { const createTooltipData = () => { // Format value based on display type to avoid redundant percentage display @@ -37,7 +35,6 @@ export const useGaugeChartTooltip = ({ value, dotColor: colorScheme.solid, }, - linkTo: to, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/CustomCrosshairLayer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/CustomCrosshairLayer.tsx index b5c82da7c7..71dcde63d0 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/CustomCrosshairLayer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/CustomCrosshairLayer.tsx @@ -30,6 +30,7 @@ type CustomCrosshairLayerProps = { innerHeight: number; innerWidth: number; onSliceHover: (data: SliceHoverData) => void; + onSliceClick?: (data: SliceHoverData) => void; onRectLeave: (relatedTarget: EventTarget | null) => void; }; @@ -38,6 +39,7 @@ export const CustomCrosshairLayer = ({ innerHeight, innerWidth, onSliceHover, + onSliceClick, onRectLeave, }: CustomCrosshairLayerProps) => { const theme = useTheme(); @@ -65,12 +67,12 @@ export const CustomCrosshairLayer = ({ .sort((sliceA, sliceB) => sliceA.x - sliceB.x); }, [points]); - const handleMouseMove = useCallback( + const buildSliceData = useCallback( (event: MouseEvent) => { const svgRect = event.currentTarget.ownerSVGElement?.getBoundingClientRect(); if (!isDefined(svgRect)) { - return; + return null; } const mouseX = event.clientX - svgRect.left - LINE_CHART_MARGIN_LEFT; @@ -82,10 +84,6 @@ export const CustomCrosshairLayer = ({ return currentDistance < nearestDistance ? slice : nearest; }); - if (nearestSlice.x === crosshairX) { - return; - } - const closestPoint = nearestSlice.points.reduce( (closestPointCandidate, pointCandidate) => { const currentDistance = Math.abs(pointCandidate.y - mouseY); @@ -96,15 +94,44 @@ export const CustomCrosshairLayer = ({ }, ); - onSliceHover({ + return { sliceX: nearestSlice.x, mouseY, nearestSlice, closestPoint, svgRect, - }); + }; }, - [slices, crosshairX, onSliceHover], + [slices], + ); + + const handleMouseMove = (event: MouseEvent) => { + const sliceData = buildSliceData(event); + if (!isDefined(sliceData)) { + return; + } + + if (sliceData.sliceX === crosshairX) { + return; + } + + onSliceHover(sliceData); + }; + + const handleClick = useCallback( + (event: MouseEvent) => { + if (!isDefined(onSliceClick)) { + return; + } + + const sliceData = buildSliceData(event); + if (!isDefined(sliceData)) { + return; + } + + onSliceClick(sliceData); + }, + [buildSliceData, onSliceClick], ); const transition = { @@ -143,6 +170,7 @@ export const CustomCrosshairLayer = ({ onMouseEnter={handleMouseMove} onMouseMove={handleMouseMove} onMouseLeave={(event) => onRectLeave(event.relatedTarget)} + onClick={handleClick} /> ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphLineChartTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphLineChartTooltip.tsx index 61d7725564..dc69ff5f26 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphLineChartTooltip.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphLineChartTooltip.tsx @@ -5,12 +5,14 @@ import { getLineChartTooltipData } from '@/page-layout/widgets/graph/graphWidget import { getTooltipReferenceFromLineChartPointAnchor } from '@/page-layout/widgets/graph/utils/getTooltipReferenceFromLineChartPointAnchor'; import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { type LineSeries, type Point } from '@nivo/line'; import { isDefined } from 'twenty-shared/utils'; type GraphLineChartTooltipProps = { containerId: string; enrichedSeries: LineChartEnrichedSeries[]; formatOptions: GraphValueFormatOptions; + onSliceClick?: (point: Point) => void; onMouseEnter?: () => void; onMouseLeave?: () => void; }; @@ -19,12 +21,29 @@ export const GraphLineChartTooltip = ({ containerId, enrichedSeries, formatOptions, + onSliceClick, onMouseEnter, onMouseLeave, }: GraphLineChartTooltipProps) => { const tooltipState = useRecoilComponentValue( graphWidgetLineTooltipComponentState, ); + + const handleTooltipClick: (() => void) | undefined = isDefined(onSliceClick) + ? () => { + if (!isDefined(tooltipState)) return; + + const highlightedPoint = tooltipState.slice.points.find( + (point) => + String(point.seriesId) === tooltipState.highlightedSeriesId, + ); + + if (!isDefined(highlightedPoint)) return; + + onSliceClick(highlightedPoint); + } + : undefined; + const tooltipData = !isDefined(tooltipState) ? null : getLineChartTooltipData({ @@ -66,7 +85,7 @@ export const GraphLineChartTooltip = ({ items={tooltipData.items} indexLabel={tooltipData.indexLabel} highlightedKey={tooltipState?.highlightedSeriesId} - linkTo={tooltipState?.linkTo} + onGraphWidgetTooltipClick={handleTooltipClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} /> diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx index 43b5b79976..cf44cf81df 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx @@ -32,9 +32,11 @@ import { ResponsiveLine, type LineCustomSvgLayerProps, type LineSeries, + type Point, type SliceTooltipProps, } from '@nivo/line'; import { useCallback, useId, useRef, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; import { useDebouncedCallback } from 'use-debounce'; type CrosshairLayerProps = LineCustomSvgLayerProps; @@ -53,6 +55,7 @@ type GraphWidgetLineChartProps = { rangeMax?: number; omitNullValues?: boolean; groupMode?: 'stacked'; + onSliceClick?: (point: Point) => void; } & GraphValueFormatOptions; const StyledContainer = styled.div` @@ -82,6 +85,7 @@ export const GraphWidgetLineChart = ({ prefix, suffix, customFormatter, + onSliceClick, }: GraphWidgetLineChartProps) => { const theme = useTheme(); const instanceId = useId(); @@ -102,23 +106,17 @@ export const GraphWidgetLineChart = ({ const effectiveMinimumValue = rangeMin ?? calculatedValueRange.minimum; const effectiveMaximumValue = rangeMax ?? calculatedValueRange.maximum; - const { - dataMap, - enrichedSeries, - nivoData, - defs, - fill, - colors, - legendItems, - hasClickableItems, - } = useLineChartData({ - data, - colorRegistry, - id, - instanceId, - enableArea, - theme, - }); + const { enrichedSeries, nivoData, defs, fill, colors, legendItems } = + useLineChartData({ + data, + colorRegistry, + id, + instanceId, + enableArea, + theme, + }); + + const hasClickableItems = isDefined(onSliceClick); const setActiveLineTooltip = useSetRecoilComponentState( graphWidgetLineTooltipComponentState, @@ -141,38 +139,34 @@ export const GraphWidgetLineChart = ({ const handleTooltipMouseLeave = debouncedHideTooltip; - const handleSliceHover = useCallback( - (sliceData: SliceHoverData) => { - const slice: SliceTooltipProps['slice'] = { - id: String(sliceData.nearestSlice.xValue ?? ''), - x: sliceData.nearestSlice.x, - y: sliceData.mouseY, - x0: sliceData.nearestSlice.x, - y0: 0, - width: 0, - height: 0, - points: sliceData.nearestSlice.points, - }; + const handleSliceLeave = () => { + debouncedHideTooltip(); + }; - const offsetLeft = sliceData.nearestSlice.x + LINE_CHART_MARGIN_LEFT; - const offsetTop = sliceData.mouseY + LINE_CHART_MARGIN_TOP; + const handleSliceEnter = (sliceData: SliceHoverData) => { + const slice: SliceTooltipProps['slice'] = { + id: String(sliceData.nearestSlice.xValue ?? ''), + x: sliceData.nearestSlice.x, + y: sliceData.mouseY, + x0: sliceData.nearestSlice.x, + y0: 0, + width: 0, + height: 0, + points: sliceData.nearestSlice.points, + }; - const seriesForLink = dataMap[String(sliceData.closestPoint.seriesId)]; - const linkTo = - seriesForLink?.data?.[sliceData.closestPoint.indexInSeries]?.to; + const offsetLeft = sliceData.nearestSlice.x + LINE_CHART_MARGIN_LEFT; + const offsetTop = sliceData.mouseY + LINE_CHART_MARGIN_TOP; - debouncedHideTooltip.cancel(); - setCrosshairX(sliceData.sliceX); - setActiveLineTooltip({ - slice, - offsetLeft, - offsetTop, - highlightedSeriesId: String(sliceData.closestPoint.seriesId), - linkTo, - }); - }, - [dataMap, debouncedHideTooltip, setActiveLineTooltip, setCrosshairX], - ); + debouncedHideTooltip.cancel(); + setCrosshairX(sliceData.sliceX); + setActiveLineTooltip({ + slice, + offsetLeft, + offsetTop, + highlightedSeriesId: String(sliceData.closestPoint.seriesId), + }); + }; const PointLabelsLayer = (layerProps: PointLabelsLayerProps) => ( debouncedHideTooltip()} + onSliceHover={handleSliceEnter} + onSliceClick={ + isDefined(onSliceClick) + ? (sliceData) => onSliceClick(sliceData.closestPoint) + : undefined + } + onRectLeave={handleSliceLeave} /> ); @@ -276,6 +275,7 @@ export const GraphWidgetLineChart = ({ containerId={id} enrichedSeries={enrichedSeries} formatOptions={formatOptions} + onSliceClick={onSliceClick} onMouseEnter={handleTooltipMouseEnter} onMouseLeave={handleTooltipMouseLeave} /> diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx index 750aec3248..fddc21fd23 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx @@ -3,8 +3,13 @@ import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/gr import { LINE_CHART_IS_STACKED_DEFAULT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartIsStackedDefault'; import { useGraphLineChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData'; import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey'; +import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector'; +import { type LineSeries, type Point } from '@nivo/line'; import { lazy, Suspense } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import { AppPath } from 'twenty-shared/types'; +import { getAppPath, isDefined } from 'twenty-shared/utils'; import { type LineChartConfiguration, type PageLayoutWidget, @@ -30,11 +35,13 @@ export const GraphWidgetLineChartRenderer = ({ showDataLabels, hasTooManyGroups, loading, + objectMetadataItem, } = useGraphLineChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, configuration: widget.configuration as LineChartConfiguration, }); + const navigate = useNavigate(); const configuration = widget.configuration as LineChartConfiguration; const hasGroupByOnSecondaryAxis = isDefined( @@ -53,6 +60,22 @@ export const GraphWidgetLineChartRenderer = ({ configuration.omitNullValues, ); + const indexViewId = useRecoilValue( + coreIndexViewIdFromObjectMetadataItemFamilySelector({ + objectMetadataItemId: objectMetadataItem.id, + }), + ); + + const handlePointClick = (_point: Point) => { + return navigate( + getAppPath( + AppPath.RecordIndexPage, + { objectNamePlural: objectMetadataItem.namePlural }, + isDefined(indexViewId) ? { viewId: indexViewId } : undefined, + ), + ); + }; + if (loading) { return ; } @@ -74,6 +97,7 @@ export const GraphWidgetLineChartRenderer = ({ omitNullValues={configuration.omitNullValues ?? false} groupMode={groupMode} displayType="shortNumber" + onSliceClick={handlePointClick} /> ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts index d762c912ea..9573a7523c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts @@ -2,6 +2,7 @@ import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLin import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; import { renderHook } from '@testing-library/react'; import { type ThemeType } from 'twenty-ui/theme'; + import { useLineChartData } from '../useLineChartData'; describe('useLineChartData', () => { @@ -76,22 +77,6 @@ describe('useLineChartData', () => { }, ]; - it('should create data map', () => { - const { result } = renderHook(() => - useLineChartData({ - data: mockData, - colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - enableArea: false, - theme: mockTheme, - }), - ); - - expect(result.current.dataMap.series1).toBe(mockData[0]); - expect(result.current.dataMap.series2).toBe(mockData[1]); - }); - it('should enrich series with color schemes', () => { const { result } = renderHook(() => useLineChartData({ @@ -305,31 +290,6 @@ describe('useLineChartData', () => { ]); }); - it('should detect clickable items', () => { - const dataWithLinks: LineChartSeries[] = [ - { - id: 'series1', - data: [ - { x: 'Jan', y: 100, to: '/january' }, - { x: 'Feb', y: 120 }, - ], - }, - ]; - - const { result } = renderHook(() => - useLineChartData({ - data: dataWithLinks, - colorRegistry: mockColorRegistry, - id: 'test-chart', - instanceId: 'instance-1', - enableArea: false, - theme: mockTheme, - }), - ); - - expect(result.current.hasClickableItems).toBe(true); - }); - it('should handle empty data', () => { const { result } = renderHook(() => useLineChartData({ @@ -342,14 +302,12 @@ describe('useLineChartData', () => { }), ); - expect(result.current.dataMap).toEqual({}); expect(result.current.enrichedSeries).toEqual([]); expect(result.current.nivoData).toEqual([]); expect(result.current.defs).toEqual([]); expect(result.current.fill).toEqual([]); expect(result.current.colors).toEqual([]); expect(result.current.legendItems).toEqual([]); - expect(result.current.hasClickableItems).toBe(false); }); it('should use series id as label when label is not provided', () => { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData.ts index 18d90a671e..0e68d524bf 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData.ts @@ -1,4 +1,5 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries'; import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery'; import { transformGroupByDataToLineChartData } from '@/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData'; @@ -18,6 +19,7 @@ type UseGraphLineChartWidgetDataResult = { hasTooManyGroups: boolean; loading: boolean; error?: Error; + objectMetadataItem: ObjectMetadataItem; }; export const useGraphLineChartWidgetData = ({ @@ -51,6 +53,7 @@ export const useGraphLineChartWidgetData = ({ return { ...transformedData, + objectMetadataItem, loading, error, }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts index ddab868733..5bf4999510 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts @@ -5,7 +5,6 @@ import { createGradientDef } from '@/page-layout/widgets/graph/utils/createGradi import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; import { type LineSeries } from '@nivo/line'; import { useMemo } from 'react'; -import { isDefined } from 'twenty-shared/utils'; import { type ThemeType } from 'twenty-ui/theme'; type UseLineChartDataProps = { @@ -25,7 +24,6 @@ export const useLineChartData = ({ enableArea, theme, }: UseLineChartDataProps) => { - const dataMap = Object.fromEntries(data.map((series) => [series.id, series])); const enrichedSeries = useMemo((): LineChartEnrichedSeries[] => { return data.map((series, index) => { const colorScheme = getColorScheme({ @@ -84,18 +82,12 @@ export const useLineChartData = ({ }; }); - const hasClickableItems = data.some((series) => - series.data.some((point) => isDefined(point.to)), - ); - return { - dataMap, enrichedSeries, nivoData, defs, fill, colors, legendItems, - hasClickableItems, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineTooltipComponentState.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineTooltipComponentState.ts index 3a0b28e1e2..fa9173def7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineTooltipComponentState.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineTooltipComponentState.ts @@ -7,7 +7,6 @@ export const graphWidgetLineTooltipComponentState = createComponentState<{ offsetLeft: number; offsetTop: number; highlightedSeriesId: string; - linkTo: string | undefined; } | null>({ key: 'graphWidgetLineTooltipComponentState', defaultValue: null, 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 71423086f0..889fed2e7e 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 @@ -69,7 +69,6 @@ export const GraphWidgetPieChart = ({ const { createTooltipData } = usePieChartTooltip({ enrichedData, - data, formatOptions, displayType, }); @@ -91,12 +90,7 @@ export const GraphWidgetPieChart = ({ const tooltipData = createTooltipData(datum); if (!isDefined(tooltipData)) return null; - return ( - - ); + return ; }; return ( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip.ts index 11d40bb84d..6a3b5cec29 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartTooltip.ts @@ -1,4 +1,3 @@ -import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem'; import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData'; import { formatGraphValue, @@ -9,14 +8,12 @@ import { isDefined } from 'twenty-shared/utils'; type UsePieChartTooltipProps = { enrichedData: PieChartEnrichedData[]; - data: PieChartDataItem[]; formatOptions: GraphValueFormatOptions; displayType?: string; }; export const usePieChartTooltip = ({ enrichedData, - data, formatOptions, displayType, }: UsePieChartTooltipProps) => { @@ -28,10 +25,6 @@ export const usePieChartTooltip = ({ ); if (!isDefined(item)) return null; - const dataItem = data.find( - (dataItemCandidate) => dataItemCandidate.id === datum.id, - ); - const formattedValue = displayType === 'percentage' ? formatGraphValue(item.percentage / 100, formatOptions) @@ -45,7 +38,6 @@ export const usePieChartTooltip = ({ value: item.value, dotColor: item.colorScheme.solid, }, - linkTo: dataItem?.to, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphWidgetTooltipContent.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphWidgetTooltipContent.ts index c31df04ad1..92133e4883 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphWidgetTooltipContent.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphWidgetTooltipContent.ts @@ -4,5 +4,4 @@ export type GraphWidgetTooltipContent = { items: GraphWidgetTooltipItem[]; indexLabel?: string; highlightedKey?: string; - linkTo?: string; };