Connect the number chart to the backend (#15229)
https://github.com/user-attachments/assets/e8fecb5d-bc9b-425a-9c19-baca15f88b1c
This commit is contained in:
+2
-7
@@ -3,7 +3,7 @@ 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 { GraphWidgetBarChartRenderer } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer';
|
||||
import { GraphWidgetNumberChart } from '@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart';
|
||||
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';
|
||||
@@ -64,12 +64,7 @@ export const GraphWidget = ({
|
||||
|
||||
switch (graphType) {
|
||||
case GraphType.NUMBER:
|
||||
return (
|
||||
<GraphWidgetNumberChart
|
||||
value={data.value}
|
||||
trendPercentage={data.trendPercentage}
|
||||
/>
|
||||
);
|
||||
return <GraphWidgetNumberChartRenderer widget={widget} />;
|
||||
|
||||
case GraphType.GAUGE:
|
||||
return (
|
||||
|
||||
-6
@@ -1,7 +1,6 @@
|
||||
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
|
||||
import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type PageLayoutWidget,
|
||||
@@ -30,7 +29,6 @@ export const GraphWidgetBarChartRenderer = ({
|
||||
showDataLabels,
|
||||
layout,
|
||||
loading,
|
||||
error,
|
||||
} = useGraphBarChartWidgetData({
|
||||
objectMetadataItemId: widget.objectMetadataId,
|
||||
configuration: widget.configuration as BarChartConfiguration,
|
||||
@@ -40,10 +38,6 @@ export const GraphWidgetBarChartRenderer = ({
|
||||
return <ChartSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (isDefined(error)) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
const configuration = widget.configuration as BarChartConfiguration;
|
||||
const groupMode =
|
||||
configuration.groupMode === 'GROUPED' ? 'grouped' : 'stacked';
|
||||
|
||||
+25
-19
@@ -1,6 +1,7 @@
|
||||
import { formatNumberChartTrend } from '@/page-layout/widgets/graph/graphWidgetNumberChart/utils/formatNumberChartTrend';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
@@ -9,8 +10,8 @@ import {
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
type GraphWidgetNumberChartProps = {
|
||||
value: string;
|
||||
trendPercentage: number;
|
||||
value: string | number;
|
||||
trendPercentage?: number;
|
||||
};
|
||||
|
||||
const StyledTrendPercentageValue = styled.span`
|
||||
@@ -43,27 +44,32 @@ export const GraphWidgetNumberChart = ({
|
||||
trendPercentage,
|
||||
}: GraphWidgetNumberChartProps) => {
|
||||
const theme = useTheme();
|
||||
const formattedPercentage = formatNumberChartTrend(trendPercentage);
|
||||
|
||||
const formattedPercentage = isDefined(trendPercentage)
|
||||
? formatNumberChartTrend(trendPercentage)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledH1Title title={value} fontColor={H1TitleFontColor.Primary} />
|
||||
<StyledTrendIconContainer>
|
||||
<StyledTrendPercentageValue>
|
||||
{formattedPercentage}%
|
||||
</StyledTrendPercentageValue>
|
||||
{trendPercentage >= 0 ? (
|
||||
<IconTrendingUp
|
||||
color={theme.color.turquoise40}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
) : (
|
||||
<IconTrendingDown
|
||||
color={theme.adaptiveColors.red4}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
)}
|
||||
</StyledTrendIconContainer>
|
||||
{isDefined(trendPercentage) && (
|
||||
<StyledTrendIconContainer>
|
||||
<StyledTrendPercentageValue>
|
||||
{formattedPercentage}%
|
||||
</StyledTrendPercentageValue>
|
||||
{trendPercentage >= 0 ? (
|
||||
<IconTrendingUp
|
||||
color={theme.color.turquoise40}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
) : (
|
||||
<IconTrendingDown
|
||||
color={theme.adaptiveColors.red4}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
)}
|
||||
</StyledTrendIconContainer>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
|
||||
import { useGraphWidgetAggregateQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetAggregateQuery';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import {
|
||||
type NumberChartConfiguration,
|
||||
type PageLayoutWidget,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const GraphWidgetNumberChart = lazy(() =>
|
||||
import(
|
||||
'@/page-layout/widgets/graph/graphWidgetNumberChart/components/GraphWidgetNumberChart'
|
||||
).then((module) => ({
|
||||
default: module.GraphWidgetNumberChart,
|
||||
})),
|
||||
);
|
||||
|
||||
export const GraphWidgetNumberChartRenderer = ({
|
||||
widget,
|
||||
}: {
|
||||
widget: PageLayoutWidget;
|
||||
}) => {
|
||||
const { value, loading } = useGraphWidgetAggregateQuery({
|
||||
objectMetadataItemId: widget.objectMetadataId,
|
||||
configuration: widget.configuration as NumberChartConfiguration,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <ChartSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ChartSkeletonLoader />}>
|
||||
<GraphWidgetNumberChart value={value ?? 0} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
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 { 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 { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
|
||||
export const useGraphWidgetAggregateQuery = ({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
}: {
|
||||
objectMetadataItemId: string;
|
||||
configuration: NumberChartConfiguration;
|
||||
}) => {
|
||||
const { objectMetadataItem, gqlOperationFilter, aggregateField } =
|
||||
useGraphWidgetQueryCommon({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
});
|
||||
|
||||
// TODO: Move this enum to shared
|
||||
const aggregateOperation =
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations;
|
||||
|
||||
const { data, loading, error } = useAggregateRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
recordGqlFieldsAggregate: {
|
||||
[aggregateField.name]: [aggregateOperation],
|
||||
},
|
||||
filter: gqlOperationFilter,
|
||||
});
|
||||
|
||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||
const dateLocale = useRecoilValue(dateLocaleState);
|
||||
|
||||
const { value, label } = computeAggregateValueAndLabel({
|
||||
data,
|
||||
objectMetadataItem,
|
||||
fieldMetadataId: configuration.aggregateFieldMetadataId,
|
||||
aggregateOperation,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
timeZone,
|
||||
localeCatalog: dateLocale.localeCatalog,
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
label,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
+14
-32
@@ -1,14 +1,11 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { getAvailableAggregationsFromObjectFields } from '@/object-record/utils/getAvailableAggregationsFromObjectFields';
|
||||
import { useGraphWidgetQueryCommon } from '@/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon';
|
||||
import { generateGroupByQuery } from '@/page-layout/widgets/graph/utils/generateGroupByQuery';
|
||||
import { generateGroupByQueryVariablesFromBarChartConfiguration } from '@/page-layout/widgets/graph/utils/generateGroupByQueryVariablesFromBarChartConfiguration';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
computeRecordGqlOperationFilter,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGraphWidgetGroupByQuery = ({
|
||||
@@ -18,9 +15,15 @@ export const useGraphWidgetGroupByQuery = ({
|
||||
objectMetadataItemId: string;
|
||||
configuration: BarChartConfiguration;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
const { objectMetadataItem, aggregateField, gqlOperationFilter } =
|
||||
useGraphWidgetQueryCommon({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
});
|
||||
|
||||
if (!isDefined(aggregateField)) {
|
||||
throw new Error('Aggregate field not found');
|
||||
}
|
||||
|
||||
const availableAggregations = useMemo(
|
||||
() =>
|
||||
@@ -30,16 +33,6 @@ export const useGraphWidgetGroupByQuery = ({
|
||||
[objectMetadataItem.readableFields],
|
||||
);
|
||||
|
||||
const aggregateFieldId = configuration.aggregateFieldMetadataId;
|
||||
|
||||
const aggregateField = objectMetadataItem.readableFields.find(
|
||||
(field) => field.id === aggregateFieldId,
|
||||
);
|
||||
|
||||
if (!isDefined(aggregateField)) {
|
||||
throw new Error('Aggregate field not found');
|
||||
}
|
||||
|
||||
const aggregateOperation =
|
||||
availableAggregations[aggregateField.name]?.[
|
||||
configuration.aggregateOperation
|
||||
@@ -49,27 +42,16 @@ export const useGraphWidgetGroupByQuery = ({
|
||||
throw new Error('Aggregate operation not found');
|
||||
}
|
||||
|
||||
const gqlOperationFilter = computeRecordGqlOperationFilter({
|
||||
fields: objectMetadataItem.fields,
|
||||
filterValueDependencies: {},
|
||||
recordFilters: configuration.filter?.recordFilters ?? [],
|
||||
recordFilterGroups: configuration.filter?.recordFilterGroups ?? [],
|
||||
});
|
||||
|
||||
const filterQueryVariables = {
|
||||
filter: gqlOperationFilter,
|
||||
};
|
||||
|
||||
const groupByQueryVariables =
|
||||
generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem,
|
||||
barChartConfiguration: configuration,
|
||||
aggregateOperation,
|
||||
aggregateOperation: aggregateOperation,
|
||||
});
|
||||
|
||||
const variables = {
|
||||
...groupByQueryVariables,
|
||||
...filterQueryVariables,
|
||||
filter: gqlOperationFilter,
|
||||
};
|
||||
|
||||
const query = generateGroupByQuery({
|
||||
@@ -89,6 +71,6 @@ export const useGraphWidgetGroupByQuery = ({
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
aggregateOperation,
|
||||
aggregateOperation: aggregateOperation,
|
||||
};
|
||||
};
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import {
|
||||
computeRecordGqlOperationFilter,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type NumberChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const useGraphWidgetQueryCommon = ({
|
||||
objectMetadataItemId,
|
||||
configuration,
|
||||
}: {
|
||||
objectMetadataItemId: string;
|
||||
configuration: BarChartConfiguration | NumberChartConfiguration;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
|
||||
const aggregateFieldId = configuration.aggregateFieldMetadataId;
|
||||
|
||||
const aggregateField = objectMetadataItem.readableFields.find(
|
||||
(field) => field.id === aggregateFieldId,
|
||||
);
|
||||
|
||||
if (!isDefined(aggregateField)) {
|
||||
throw new Error('Aggregate field not found');
|
||||
}
|
||||
|
||||
const gqlOperationFilter = computeRecordGqlOperationFilter({
|
||||
fields: objectMetadataItem.fields,
|
||||
filterValueDependencies: {},
|
||||
recordFilters: configuration.filter?.recordFilters ?? [],
|
||||
recordFilterGroups: configuration.filter?.recordFilterGroups ?? [],
|
||||
});
|
||||
|
||||
return {
|
||||
objectMetadataItem,
|
||||
gqlOperationFilter,
|
||||
aggregateField,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user