Add a banner in the side panel to alert if the bar chart has too many bars (#15267)
Closes https://github.com/twentyhq/core-team-issues/issues/1714 - Created `SidePanelInformationBanner` component - Created a component state `hasWidgetTooManyGroupsComponentState` - Displayed the banner in the side panel if the state is true https://github.com/user-attachments/assets/343a4053-f0d5-4e9b-935d-ead191d70eb2
This commit is contained in:
+24
-1
@@ -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 && (
|
||||
<StyledSidePanelInformationBanner
|
||||
message={t`Max ${GRAPH_MAXIMUM_NUMBER_OF_GROUPS} bars per chart. Consider adding a filter`}
|
||||
/>
|
||||
)}
|
||||
{chartSettings.map((group) => (
|
||||
<CommandGroup key={group.heading} heading={group.heading}>
|
||||
{group.items.map((item) => {
|
||||
|
||||
+5
-2
@@ -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 (
|
||||
<>
|
||||
<GraphWidgetComponentInstanceContext.Provider
|
||||
value={{ instanceId: widgetInEditMode.id }}
|
||||
>
|
||||
<SidePanelHeader
|
||||
Icon={GRAPH_TYPE_INFORMATION[currentGraphType].icon}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
@@ -68,6 +71,6 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
|
||||
}}
|
||||
/>
|
||||
<ChartSettings widget={widgetInEditMode} />
|
||||
</>
|
||||
</GraphWidgetComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+10
-5
@@ -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 (
|
||||
<GraphWidget
|
||||
widget={widget}
|
||||
objectMetadataId={widget.objectMetadataId}
|
||||
graphType={graphType}
|
||||
/>
|
||||
<GraphWidgetComponentInstanceContext.Provider
|
||||
value={{ instanceId: widget.id }}
|
||||
>
|
||||
<GraphWidget
|
||||
widget={widget}
|
||||
objectMetadataId={widget.objectMetadataId}
|
||||
graphType={graphType}
|
||||
/>
|
||||
</GraphWidgetComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+21
@@ -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;
|
||||
};
|
||||
+24
-17
@@ -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 (
|
||||
<Suspense fallback={<ChartSkeletonLoader />}>
|
||||
<GraphWidgetBarChart
|
||||
key={filterStateKey}
|
||||
data={data}
|
||||
series={series}
|
||||
indexBy={indexBy}
|
||||
keys={keys}
|
||||
xAxisLabel={xAxisLabel}
|
||||
yAxisLabel={yAxisLabel}
|
||||
showValues={showDataLabels}
|
||||
layout={layout}
|
||||
groupMode={groupMode}
|
||||
id={widget.id}
|
||||
displayType="shortNumber"
|
||||
rangeMin={configuration.rangeMin ?? undefined}
|
||||
rangeMax={configuration.rangeMax ?? undefined}
|
||||
<>
|
||||
<GraphWidgetBarChartHasTooManyGroupsEffect
|
||||
hasTooManyGroups={hasTooManyGroups}
|
||||
/>
|
||||
</Suspense>
|
||||
<Suspense fallback={<ChartSkeletonLoader />}>
|
||||
<GraphWidgetBarChart
|
||||
key={filterStateKey}
|
||||
data={data}
|
||||
series={series}
|
||||
indexBy={indexBy}
|
||||
keys={keys}
|
||||
xAxisLabel={xAxisLabel}
|
||||
yAxisLabel={yAxisLabel}
|
||||
showValues={showDataLabels}
|
||||
layout={layout}
|
||||
groupMode={groupMode}
|
||||
id={widget.id}
|
||||
displayType="shortNumber"
|
||||
rangeMin={configuration.rangeMin ?? undefined}
|
||||
rangeMax={configuration.rangeMax ?? undefined}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ type UseGraphBarChartWidgetDataResult = {
|
||||
layout?: 'vertical' | 'horizontal';
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
hasTooManyGroups: boolean;
|
||||
};
|
||||
|
||||
export const useGraphBarChartWidgetData = ({
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const GraphWidgetComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
+8
@@ -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,
|
||||
});
|
||||
+2
@@ -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 = ({
|
||||
|
||||
+2
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+5
@@ -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<string>();
|
||||
const yValues = new Set<string>();
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<StyledBanner className={className}>
|
||||
<StyledIconContainer>
|
||||
<IconInfoCircle size={16} />
|
||||
</StyledIconContainer>
|
||||
<StyledMessage>{message}</StyledMessage>
|
||||
</StyledBanner>
|
||||
);
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from '@ui/testing';
|
||||
import { SidePanelInformationBanner } from '../SidePanelInformationBanner';
|
||||
|
||||
const meta: Meta<typeof SidePanelInformationBanner> = {
|
||||
title: 'UI/Display/Banner/SidePanelInformationBanner',
|
||||
component: SidePanelInformationBanner,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SidePanelInformationBanner>;
|
||||
|
||||
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.',
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user