Convert number chart to aggregate chart allowing date aggregates (#15294)

- Convert number chart to aggregate chart 
- Allow date aggregates on all charts
- Only allow `EARLIEST` and `LATEST` on Aggregate chart and Gauge chart
- Various design fixes


https://github.com/user-attachments/assets/b5a2239d-7b11-48b5-93e6-98ceffae5cab
This commit is contained in:
Raphaël Bosi
2025-10-23 18:48:42 +02:00
committed by GitHub
parent 6f45c47435
commit f301f07e43
45 changed files with 205 additions and 151 deletions
@@ -2,8 +2,8 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMeta
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 { GraphWidgetAggregateChartRenderer } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/components/GraphWidgetAggregateChartRenderer';
import { GraphWidgetBarChartRenderer } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer';
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';
@@ -63,8 +63,8 @@ export const GraphWidget = ({
}
switch (graphType) {
case GraphType.NUMBER:
return <GraphWidgetNumberChartRenderer widget={widget} />;
case GraphType.AGGREGATE:
return <GraphWidgetAggregateChartRenderer widget={widget} />;
case GraphType.GAUGE:
return (
@@ -1,16 +1,16 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { GraphWidgetAggregateChart } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/components/GraphWidgetAggregateChart';
import { ComponentDecorator } from 'twenty-ui/testing';
import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart';
const meta: Meta<typeof GraphWidgetNumberChart> = {
title: 'Modules/PageLayout/Widgets/GraphWidgetNumberChart',
component: GraphWidgetNumberChart,
const meta: Meta<typeof GraphWidgetAggregateChart> = {
title: 'Modules/PageLayout/Widgets/GraphWidgetAggregateChart',
component: GraphWidgetAggregateChart,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof GraphWidgetNumberChart>;
type Story = StoryObj<typeof GraphWidgetAggregateChart>;
export const Default: Story = {
args: {
@@ -1,4 +1,4 @@
import { formatNumberChartTrend } from '@/page-layout/widgets/graph/graphWidgetNumberChart/utils/formatNumberChartTrend';
import { formatNumberChartTrend } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/utils/formatNumberChartTrend';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
@@ -9,7 +9,7 @@ import {
IconTrendingUp,
} from 'twenty-ui/display';
type GraphWidgetNumberChartProps = {
type GraphWidgetAggregateChartProps = {
value: string | number;
trendPercentage?: number;
};
@@ -36,13 +36,14 @@ const StyledTrendIconContainer = styled.div`
`;
const StyledH1Title = styled(H1Title)`
font-size: ${({ theme }) => theme.font.size.xxl};
margin: 0;
`;
export const GraphWidgetNumberChart = ({
export const GraphWidgetAggregateChart = ({
value,
trendPercentage,
}: GraphWidgetNumberChartProps) => {
}: GraphWidgetAggregateChartProps) => {
const theme = useTheme();
const formattedPercentage = isDefined(trendPercentage)
@@ -2,26 +2,26 @@ import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/Char
import { useGraphWidgetAggregateQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetAggregateQuery';
import { lazy, Suspense } from 'react';
import {
type NumberChartConfiguration,
type AggregateChartConfiguration,
type PageLayoutWidget,
} from '~/generated/graphql';
const GraphWidgetNumberChart = lazy(() =>
const GraphWidgetAggregateChart = lazy(() =>
import(
'@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart'
'@/page-layout/widgets/graph/graphWidgetAggregateChart/components/GraphWidgetAggregateChart'
).then((module) => ({
default: module.GraphWidgetNumberChart,
default: module.GraphWidgetAggregateChart,
})),
);
export const GraphWidgetNumberChartRenderer = ({
export const GraphWidgetAggregateChartRenderer = ({
widget,
}: {
widget: PageLayoutWidget;
}) => {
const { value, loading } = useGraphWidgetAggregateQuery({
objectMetadataItemId: widget.objectMetadataId,
configuration: widget.configuration as NumberChartConfiguration,
configuration: widget.configuration as AggregateChartConfiguration,
});
if (loading) {
@@ -30,7 +30,7 @@ export const GraphWidgetNumberChartRenderer = ({
return (
<Suspense fallback={<ChartSkeletonLoader />}>
<GraphWidgetNumberChart value={value ?? 0} />
<GraphWidgetAggregateChart value={value ?? '-'} />
</Suspense>
);
};
@@ -1,11 +1,11 @@
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 { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
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 { type AggregateChartConfiguration } from '~/generated/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
export const useGraphWidgetAggregateQuery = ({
@@ -13,7 +13,7 @@ export const useGraphWidgetAggregateQuery = ({
configuration,
}: {
objectMetadataItemId: string;
configuration: NumberChartConfiguration;
configuration: AggregateChartConfiguration;
}) => {
const { objectMetadataItem, gqlOperationFilter, aggregateField } =
useGraphWidgetQueryCommon({
@@ -21,14 +21,18 @@ export const useGraphWidgetAggregateQuery = ({
configuration,
});
// TODO: Move this enum to shared
const aggregateOperation =
configuration.aggregateOperation as unknown as ExtendedAggregateOperations;
const aggregateOperation = configuration.aggregateOperation;
const extendedAggregateOperation =
convertAggregateOperationToExtendedAggregateOperation(
aggregateOperation,
aggregateField.type,
);
const { data, loading, error } = useAggregateRecords({
objectNameSingular: objectMetadataItem.nameSingular,
recordGqlFieldsAggregate: {
[aggregateField.name]: [aggregateOperation],
[aggregateField.name]: [extendedAggregateOperation],
},
filter: gqlOperationFilter,
});
@@ -40,7 +44,7 @@ export const useGraphWidgetAggregateQuery = ({
data,
objectMetadataItem,
fieldMetadataId: configuration.aggregateFieldMetadataId,
aggregateOperation,
aggregateOperation: extendedAggregateOperation,
dateFormat,
timeFormat,
timeZone,
@@ -4,8 +4,8 @@ import {
isDefined,
} from 'twenty-shared/utils';
import {
type AggregateChartConfiguration,
type BarChartConfiguration,
type NumberChartConfiguration,
} from '~/generated/graphql';
export const useGraphWidgetQueryCommon = ({
@@ -13,7 +13,7 @@ export const useGraphWidgetQueryCommon = ({
configuration,
}: {
objectMetadataItemId: string;
configuration: BarChartConfiguration | NumberChartConfiguration;
configuration: BarChartConfiguration | AggregateChartConfiguration;
}) => {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId,
@@ -53,7 +53,7 @@ export const areChartConfigurationFieldsValidForQuery = (
fieldExists(configuration.groupByFieldMetadataId, objectMetadataItem)
);
case 'NumberChartConfiguration':
case 'AggregateChartConfiguration':
case 'GaugeChartConfiguration':
return fieldExists(
configuration.aggregateFieldMetadataId,