diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx index 4e35aeb51b..930649b427 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx @@ -16,11 +16,20 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; import { isChartSettingDisabled } from '@/command-menu/pages/page-layout/utils/isChartSettingDisabled'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { GRAPH_MAXIMUM_NUMBER_OF_GROUPS } from '@/page-layout/widgets/graph/constants/GraphMaximumNumberOfGroups.constant'; +import { hasWidgetTooManyGroupsComponentState } from '@/page-layout/widgets/graph/states/hasWidgetTooManyGroupsComponentState'; import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; +import { SidePanelInformationBanner } from 'twenty-ui/display'; -import { type GraphType, type PageLayoutWidget } from '~/generated/graphql'; +import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; + +const StyledSidePanelInformationBanner = styled(SidePanelInformationBanner)` + margin-top: ${({ theme }) => theme.spacing(2)}; +`; export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo(); @@ -56,6 +65,8 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { const isGroupByEnabled = getChartSettingsValues( CHART_CONFIGURATION_SETTING_IDS.GROUP_BY, ); + const [hasWidgetTooManyGroups, setHasWidgetTooManyGroups] = + useRecoilComponentState(hasWidgetTooManyGroupsComponentState); const handleGraphTypeChange = (graphType: GraphType) => { updateCurrentWidgetConfig({ @@ -68,6 +79,13 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { updateCommandMenuPageInfo({ pageIcon: GRAPH_TYPE_INFORMATION[graphType].icon, }); + + if ( + graphType !== GraphType.VERTICAL_BAR && + graphType !== GraphType.HORIZONTAL_BAR + ) { + setHasWidgetTooManyGroups(false); + } }; const chartSettings = GRAPH_TYPE_INFORMATION[currentGraphType].settings; @@ -83,6 +101,11 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { currentGraphType={currentGraphType} setCurrentGraphType={handleGraphTypeChange} /> + {hasWidgetTooManyGroups && ( + + )} {chartSettings.map((group) => ( {group.items.map((item) => { diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx index 173feb628d..bb23a32319 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx @@ -5,6 +5,7 @@ import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pa import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useTheme } from '@emotion/react'; import { t } from '@lingui/core/macro'; @@ -53,7 +54,9 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => { const graphTypeLabel = t(GRAPH_TYPE_INFORMATION[currentGraphType].label); return ( - <> + { }} /> - + ); }; 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 76816771cb..e9ddb1ba8b 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,5 +1,6 @@ import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay'; import { GraphWidget } from '@/page-layout/widgets/graph/components/GraphWidget'; +import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext'; import { isDefined } from 'twenty-shared/utils'; import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; @@ -22,10 +23,14 @@ export const GraphWidgetRenderer = ({ widget }: GraphWidgetRendererProps) => { } return ( - + + + ); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartHasTooManyGroupsEffect.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartHasTooManyGroupsEffect.tsx new file mode 100644 index 0000000000..915a10f42e --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartHasTooManyGroupsEffect.tsx @@ -0,0 +1,21 @@ +import { hasWidgetTooManyGroupsComponentState } from '@/page-layout/widgets/graph/states/hasWidgetTooManyGroupsComponentState'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { useEffect } from 'react'; + +type GraphWidgetBarChartHasTooManyGroupsEffectProps = { + hasTooManyGroups: boolean; +}; + +export const GraphWidgetBarChartHasTooManyGroupsEffect = ({ + hasTooManyGroups, +}: GraphWidgetBarChartHasTooManyGroupsEffectProps) => { + const setHasWidgetTooManyGroups = useSetRecoilComponentState( + hasWidgetTooManyGroupsComponentState, + ); + + useEffect(() => { + setHasWidgetTooManyGroups(hasTooManyGroups); + }, [hasTooManyGroups, setHasWidgetTooManyGroups]); + + return null; +}; 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 c3eda43c1e..95ec99c058 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,4 +1,5 @@ import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; +import { GraphWidgetBarChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartHasTooManyGroupsEffect'; import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData'; import { lazy, Suspense, useMemo } from 'react'; import { @@ -29,6 +30,7 @@ export const GraphWidgetBarChartRenderer = ({ showDataLabels, layout, loading, + hasTooManyGroups, } = useGraphBarChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, configuration: widget.configuration as BarChartConfiguration, @@ -53,23 +55,28 @@ export const GraphWidgetBarChartRenderer = ({ } return ( - }> - + - + }> + + + ); }; 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 d777a5829d..50c299f269 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 @@ -22,6 +22,7 @@ type UseGraphBarChartWidgetDataResult = { layout?: 'vertical' | 'horizontal'; loading: boolean; error?: Error; + hasTooManyGroups: boolean; }; export const useGraphBarChartWidgetData = ({ diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext.ts new file mode 100644 index 0000000000..8069d16a58 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const GraphWidgetComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/states/hasWidgetTooManyGroupsComponentState.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/states/hasWidgetTooManyGroupsComponentState.ts new file mode 100644 index 0000000000..4822d9528b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/states/hasWidgetTooManyGroupsComponentState.ts @@ -0,0 +1,8 @@ +import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const hasWidgetTooManyGroupsComponentState = createComponentState({ + key: 'hasWidgetTooManyGroupsComponentState', + defaultValue: false, + componentInstanceContext: GraphWidgetComponentInstanceContext, +}); 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 6dc061125e..f125e6c9c8 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 @@ -33,6 +33,7 @@ type TransformGroupByDataToBarChartDataResult = { yAxisLabel?: string; showDataLabels: boolean; layout?: 'vertical' | 'horizontal'; + hasTooManyGroups: boolean; }; const EMPTY_BAR_CHART_RESULT: TransformGroupByDataToBarChartDataResult = { @@ -44,6 +45,7 @@ const EMPTY_BAR_CHART_RESULT: TransformGroupByDataToBarChartDataResult = { yAxisLabel: undefined, showDataLabels: false, layout: 'vertical', + hasTooManyGroups: false, }; export const transformGroupByDataToBarChartData = ({ diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts index 1f76bd8d29..59148ffe0f 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData.ts @@ -28,6 +28,7 @@ type TransformOneDimensionalGroupByToBarChartDataResult = { indexBy: string; keys: string[]; series: BarChartSeries[]; + hasTooManyGroups: boolean; }; export const transformOneDimensionalGroupByToBarChartData = ({ @@ -87,5 +88,6 @@ export const transformOneDimensionalGroupByToBarChartData = ({ indexBy: indexByKey, keys: [aggregateField.name], series, + hasTooManyGroups: rawResults.length > GRAPH_MAXIMUM_NUMBER_OF_GROUPS, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts index 9ebca27ddf..1bb0913f87 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData.ts @@ -28,6 +28,7 @@ type TransformTwoDimensionalGroupByToBarChartDataResult = { indexBy: string; keys: string[]; series: BarChartSeries[]; + hasTooManyGroups: boolean; }; export const transformTwoDimensionalGroupByToBarChartData = ({ @@ -49,6 +50,8 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ const xValues = new Set(); const yValues = new Set(); + let hasTooManyGroups = false; + rawResults.forEach((result) => { const dimensionValues = result.groupByDimensionValues; if (!isDefined(dimensionValues) || dimensionValues.length < 2) return; @@ -75,6 +78,7 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ totalUniqueDimensions + additionalDimensions > GRAPH_MAXIMUM_NUMBER_OF_GROUPS ) { + hasTooManyGroups = true; return; } @@ -118,5 +122,6 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ indexBy: indexByKey, keys, series, + hasTooManyGroups, }; }; diff --git a/packages/twenty-ui/src/display/banner/components/SidePanelInformationBanner.tsx b/packages/twenty-ui/src/display/banner/components/SidePanelInformationBanner.tsx new file mode 100644 index 0000000000..60e617a948 --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/SidePanelInformationBanner.tsx @@ -0,0 +1,54 @@ +import styled from '@emotion/styled'; +import { IconInfoCircle } from '../../icon/components/TablerIcons'; + +const StyledBanner = styled.div` + align-items: center; + background-color: ${({ theme }) => theme.accent.secondary}; + border-radius: ${({ theme }) => theme.border.radius.md}; + box-sizing: border-box; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledIconContainer = styled.div` + align-items: center; + color: ${({ theme }) => theme.color.blue}; + display: flex; + flex-shrink: 0; + height: 16px; + justify-content: center; + width: 16px; +`; + +const StyledMessage = styled.p` + color: ${({ theme }) => theme.color.blue}; + flex-grow: 1; + font-family: ${({ theme }) => theme.font.family}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-style: normal; + font-weight: ${({ theme }) => theme.font.weight.medium}; + line-height: 1.4; + margin: 0; + min-width: 0; +`; + +export type SidePanelInformationBannerProps = { + message: string; + className?: string; +}; + +export const SidePanelInformationBanner = ({ + message, + className, +}: SidePanelInformationBannerProps) => { + return ( + + + + + {message} + + ); +}; diff --git a/packages/twenty-ui/src/display/banner/components/__stories__/SidePanelInformationBanner.stories.tsx b/packages/twenty-ui/src/display/banner/components/__stories__/SidePanelInformationBanner.stories.tsx new file mode 100644 index 0000000000..d36fa0559d --- /dev/null +++ b/packages/twenty-ui/src/display/banner/components/__stories__/SidePanelInformationBanner.stories.tsx @@ -0,0 +1,31 @@ +import { type Meta, type StoryObj } from '@storybook/react'; +import { ComponentDecorator } from '@ui/testing'; +import { SidePanelInformationBanner } from '../SidePanelInformationBanner'; + +const meta: Meta = { + title: 'UI/Display/Banner/SidePanelInformationBanner', + component: SidePanelInformationBanner, + decorators: [ComponentDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + message: 'Max 200 bars per chart. Consider adding a filter', + }, +}; + +export const ShortMessage: Story = { + args: { + message: 'Too many groups', + }, +}; + +export const LongMessage: Story = { + args: { + message: + 'The chart has exceeded the maximum number of 200 bars. Please consider adding filters to reduce the number of data points displayed.', + }, +}; diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index e2a7294c5f..1d2339a8a5 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -17,6 +17,8 @@ export type { AvatarSize } from './avatar/types/AvatarSize'; export type { AvatarType } from './avatar/types/AvatarType'; export type { BannerVariant } from './banner/components/Banner'; export { Banner } from './banner/components/Banner'; +export type { SidePanelInformationBannerProps } from './banner/components/SidePanelInformationBanner'; +export { SidePanelInformationBanner } from './banner/components/SidePanelInformationBanner'; export type { AnimatedCheckmarkProps } from './checkmark/components/AnimatedCheckmark'; export { AnimatedCheckmark } from './checkmark/components/AnimatedCheckmark'; export type { CheckmarkProps } from './checkmark/components/Checkmark';