[Dashboards] add ratio aggregate on aggregate chart (#16413)

closes https://github.com/twentyhq/core-team-issues/issues/1960
video QA


https://github.com/user-attachments/assets/e90cee44-5edc-430d-b003-03ab4ea921f7
This commit is contained in:
nitin
2025-12-09 19:53:21 +05:30
committed by GitHub
parent bd49fd8d5f
commit 7af3da9393
25 changed files with 1304 additions and 84 deletions
@@ -152,6 +152,7 @@ export type AggregateChartConfiguration = {
graphType: GraphType;
label?: Maybe<Scalars['String']>;
prefix?: Maybe<Scalars['String']>;
ratioAggregateConfig?: Maybe<RatioAggregateConfig>;
suffix?: Maybe<Scalars['String']>;
timezone?: Maybe<Scalars['String']>;
};
@@ -3695,6 +3696,17 @@ export type QueueRetentionConfig = {
failedMaxCount: Scalars['Float'];
};
export type RatioAggregateConfig = {
__typename?: 'RatioAggregateConfig';
fieldMetadataId: Scalars['UUID'];
optionValue: Scalars['String'];
};
export type RatioAggregateConfigInput = {
fieldMetadataId: Scalars['UUID'];
optionValue: Scalars['String'];
};
export type Relation = {
__typename?: 'Relation';
sourceFieldMetadata: Field;
+18 -2
View File
@@ -152,6 +152,7 @@ export type AggregateChartConfiguration = {
graphType: GraphType;
label?: Maybe<Scalars['String']>;
prefix?: Maybe<Scalars['String']>;
ratioAggregateConfig?: Maybe<RatioAggregateConfig>;
suffix?: Maybe<Scalars['String']>;
timezone?: Maybe<Scalars['String']>;
};
@@ -3546,6 +3547,17 @@ export type QueueRetentionConfig = {
failedMaxCount: Scalars['Float'];
};
export type RatioAggregateConfig = {
__typename?: 'RatioAggregateConfig';
fieldMetadataId: Scalars['UUID'];
optionValue: Scalars['String'];
};
export type RatioAggregateConfigInput = {
fieldMetadataId: Scalars['UUID'];
optionValue: Scalars['String'];
};
export type Relation = {
__typename?: 'Relation';
sourceFieldMetadata: Field;
@@ -4825,7 +4837,7 @@ export type SearchQueryVariables = Exact<{
export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResultConnection', edges: Array<{ __typename?: 'SearchResultEdge', cursor: string, node: { __typename?: 'SearchRecord', recordId: any, objectNameSingular: string, label: string, imageUrl?: string | null, tsRankCD: number, tsRank: number } }>, pageInfo: { __typename?: 'SearchResultPageInfo', hasNextPage: boolean, endCursor?: string | null } } };
export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | null };
export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: any, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | null };
export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
id: Scalars['String'];
@@ -4833,7 +4845,7 @@ export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
}>;
export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | null }> | null }> | null } };
export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: any, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | null }> | null }> | null } };
export type OnDbEventSubscriptionVariables = Exact<{
input: OnDbEventInput;
@@ -5226,6 +5238,10 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
suffix
timezone
firstDayOfTheWeek
ratioAggregateConfig {
fieldMetadataId
optionValue
}
}
... on GaugeChartConfiguration {
graphType
@@ -0,0 +1,91 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { type AggregateChartOperation } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/types/AggregateChartOperation';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isDefined } from 'twenty-shared/utils';
import { MenuItemSelect } from 'twenty-ui/navigation';
const isExtendedAggregateOperation = (
operation: AggregateChartOperation,
): operation is ExtendedAggregateOperations => {
return operation !== DASHBOARD_AGGREGATE_OPERATION_RATIO;
};
export const ChartAggregateOperationSelectableListItem = ({
operation,
label,
currentFieldMetadataId,
}: {
operation: AggregateChartOperation;
label: string;
currentFieldMetadataId: string;
}) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const configuration = widgetInEditMode?.configuration;
const currentAggregateOperation =
configuration &&
'aggregateOperation' in configuration &&
configuration.aggregateOperation;
const isCurrentlyRatio =
configuration?.__typename === 'AggregateChartConfiguration' &&
isDefined(configuration.ratioAggregateConfig);
if (!isExtendedAggregateOperation(operation)) {
return null;
}
const aggregateOperation =
convertExtendedAggregateOperationToAggregateOperation(operation);
const isSelected =
currentAggregateOperation === aggregateOperation && !isCurrentlyRatio;
const isFocused = selectedItemId === operation;
const handleClick = () => {
updateCurrentWidgetConfig({
configToUpdate: {
aggregateFieldMetadataId: currentFieldMetadataId,
aggregateOperation,
ratioAggregateConfig: null,
},
});
closeDropdown();
};
return (
<SelectableListItem itemId={operation} onEnter={handleClick}>
<MenuItemSelect
text={label}
selected={isSelected}
focused={isFocused}
onClick={handleClick}
/>
</SelectableListItem>
);
};
@@ -1,29 +1,28 @@
import { ChartAggregateOperationSelectableListItem } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartAggregateOperationSelectableListItem';
import { ChartRatioAggregateOperationSelectableListItem } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartRatioAggregateOperationSelectableListItem';
import { ChartRatioOptionValueSelectionDropdownContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartRatioOptionValueSelectionDropdownContent';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { DateAggregateOperations } from '@/object-record/record-table/constants/DateAggregateOperations';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { type AggregateChartOperation } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/types/AggregateChartOperation';
import { getAggregateChartOperationLabel } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/utils/getAggregateChartOperationLabel';
import { getAvailableAggregateOperationsForAggregateChart } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/utils/getAvailableAggregateOperationsForAggregateChart';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { GraphType, type AggregateOperations } from '~/generated/graphql';
import { GraphType } from '~/generated/graphql';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
export const ChartAggregateOperationSelectionDropdownContent = ({
@@ -34,6 +33,7 @@ export const ChartAggregateOperationSelectionDropdownContent = ({
setIsSubMenuOpen: (isSubMenuOpen: boolean) => void;
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [isOptionValueMenuOpen, setIsOptionValueMenuOpen] = useState(false);
const { objectMetadataItems } = useObjectMetadataItems();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
@@ -48,9 +48,6 @@ export const ChartAggregateOperationSelectionDropdownContent = ({
throw new Error('Invalid configuration type');
}
const currentAggregateOperation =
widgetInEditMode.configuration.aggregateOperation;
const sourceObjectMetadataItem = objectMetadataItems.find(
(item) => item.id === widgetInEditMode.objectMetadataId,
);
@@ -63,21 +60,23 @@ export const ChartAggregateOperationSelectionDropdownContent = ({
DropdownComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const availableAggregateOperations = selectedField
? getAvailableAggregateOperationsForFieldMetadataType({
fieldMetadataType: selectedField.type,
})
: [];
const isAggregateChart =
widgetInEditMode.configuration.graphType === GraphType.AGGREGATE;
const isAggregateOrGaugeChart =
widgetInEditMode.configuration.graphType === GraphType.AGGREGATE ||
isAggregateChart ||
widgetInEditMode.configuration.graphType === GraphType.GAUGE;
const availableAggregateOperations: AggregateChartOperation[] = selectedField
? isAggregateChart
? getAvailableAggregateOperationsForAggregateChart({
fieldMetadataType: selectedField.type,
})
: getAvailableAggregateOperationsForFieldMetadataType({
fieldMetadataType: selectedField.type,
})
: [];
const filteredAggregateOperations = availableAggregateOperations.filter(
(operation) => {
return (
@@ -91,7 +90,7 @@ export const ChartAggregateOperationSelectionDropdownContent = ({
const aggregateOperationsWithLabels = filteredAggregateOperations.map(
(operation) => ({
operation,
label: getAggregateOperationLabel(operation),
label: getAggregateChartOperationLabel(operation),
}),
);
@@ -101,27 +100,23 @@ export const ChartAggregateOperationSelectionDropdownContent = ({
getSearchableValues: (item) => [item.label],
});
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
if (!isDefined(sourceObjectMetadataItem) || !isDefined(selectedField)) {
return null;
}
const handleSelectAggregateOperation = (
aggregateOperation: AggregateOperations,
) => {
updateCurrentWidgetConfig({
configToUpdate: {
aggregateFieldMetadataId: currentFieldMetadataId,
aggregateOperation,
},
});
closeDropdown();
const handleSelectRatio = () => {
setIsOptionValueMenuOpen(true);
};
if (isOptionValueMenuOpen) {
return (
<ChartRatioOptionValueSelectionDropdownContent
currentFieldMetadataId={currentFieldMetadataId}
setIsOptionValueMenuOpen={setIsOptionValueMenuOpen}
/>
);
}
return (
<>
<DropdownMenuHeader
@@ -150,37 +145,22 @@ export const ChartAggregateOperationSelectionDropdownContent = ({
(item) => item.operation,
)}
>
{filteredAggregateOperationsWithLabels.map((item) => (
<SelectableListItem
key={item.operation}
itemId={item.operation}
onEnter={() => {
handleSelectAggregateOperation(
convertExtendedAggregateOperationToAggregateOperation(
item.operation,
),
);
}}
>
<MenuItemSelect
text={item.label}
selected={
currentAggregateOperation ===
convertExtendedAggregateOperationToAggregateOperation(
item.operation,
)
}
focused={selectedItemId === item.operation}
onClick={() => {
handleSelectAggregateOperation(
convertExtendedAggregateOperationToAggregateOperation(
item.operation,
),
);
}}
{filteredAggregateOperationsWithLabels.map((item) =>
item.operation === DASHBOARD_AGGREGATE_OPERATION_RATIO ? (
<ChartRatioAggregateOperationSelectableListItem
key={item.operation}
label={item.label}
onSelect={handleSelectRatio}
/>
</SelectableListItem>
))}
) : (
<ChartAggregateOperationSelectableListItem
key={item.operation}
operation={item.operation}
label={item.label}
currentFieldMetadataId={currentFieldMetadataId}
/>
),
)}
</SelectableList>
</DropdownMenuItemsContainer>
</>
@@ -101,6 +101,7 @@ export const ChartDataSourceDropdownContent = () => {
groupByFieldMetadataId: null,
groupBySubFieldName: null,
filter: {},
ratioAggregateConfig: null,
},
});
@@ -0,0 +1,52 @@
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isDefined } from 'twenty-shared/utils';
import { MenuItemSelect } from 'twenty-ui/navigation';
export const ChartRatioAggregateOperationSelectableListItem = ({
label,
onSelect,
}: {
label: string;
onSelect: () => void;
}) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const isCurrentlyRatio =
widgetInEditMode?.configuration?.__typename ===
'AggregateChartConfiguration' &&
isDefined(widgetInEditMode.configuration.ratioAggregateConfig);
const isFocused = selectedItemId === DASHBOARD_AGGREGATE_OPERATION_RATIO;
return (
<SelectableListItem
itemId={DASHBOARD_AGGREGATE_OPERATION_RATIO}
onEnter={onSelect}
>
<MenuItemSelect
text={label}
selected={isCurrentlyRatio}
focused={isFocused}
hasSubMenu={true}
onClick={onSelect}
/>
</SelectableListItem>
);
};
@@ -0,0 +1,72 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { IconCheck, IconX } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { AggregateOperations } from '~/generated/graphql';
export const ChartRatioOptionBooleanSelectableListItem = ({
optionValue,
label,
currentFieldMetadataId,
}: {
optionValue: string;
label: string;
currentFieldMetadataId: string;
}) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const currentRatioConfig =
widgetInEditMode?.configuration?.__typename ===
'AggregateChartConfiguration'
? widgetInEditMode.configuration.ratioAggregateConfig
: undefined;
const isSelected = currentRatioConfig?.optionValue === optionValue;
const isFocused = selectedItemId === optionValue;
const handleClick = () => {
updateCurrentWidgetConfig({
configToUpdate: {
aggregateFieldMetadataId: currentFieldMetadataId,
aggregateOperation: AggregateOperations.COUNT,
ratioAggregateConfig: {
fieldMetadataId: currentFieldMetadataId,
optionValue,
},
},
});
closeDropdown();
};
return (
<SelectableListItem itemId={optionValue} onEnter={handleClick}>
<MenuItemSelect
text={label}
LeftIcon={optionValue === 'true' ? IconCheck : IconX}
selected={isSelected}
focused={isFocused}
onClick={handleClick}
/>
</SelectableListItem>
);
};
@@ -0,0 +1,74 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { MenuItemSelectTag } from 'twenty-ui/navigation';
import { type ThemeColor } from 'twenty-ui/theme';
import { AggregateOperations } from '~/generated/graphql';
export const ChartRatioOptionSelectSelectableListItem = ({
optionValue,
label,
color,
currentFieldMetadataId,
}: {
optionValue: string;
label: string;
color: ThemeColor | undefined;
currentFieldMetadataId: string;
}) => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
const currentRatioConfig =
widgetInEditMode?.configuration?.__typename ===
'AggregateChartConfiguration'
? widgetInEditMode.configuration.ratioAggregateConfig
: undefined;
const isSelected = currentRatioConfig?.optionValue === optionValue;
const isFocused = selectedItemId === optionValue;
const handleClick = () => {
updateCurrentWidgetConfig({
configToUpdate: {
aggregateFieldMetadataId: currentFieldMetadataId,
aggregateOperation: AggregateOperations.COUNT,
ratioAggregateConfig: {
fieldMetadataId: currentFieldMetadataId,
optionValue,
},
},
});
closeDropdown();
};
return (
<SelectableListItem itemId={optionValue} onEnter={handleClick}>
<MenuItemSelectTag
text={label}
color={color ?? 'transparent'}
selected={isSelected}
focused={isFocused}
onClick={handleClick}
/>
</SelectableListItem>
);
};
@@ -0,0 +1,143 @@
import { ChartRatioOptionBooleanSelectableListItem } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartRatioOptionBooleanSelectableListItem';
import { ChartRatioOptionSelectSelectableListItem } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartRatioOptionSelectSelectableListItem';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft } from 'twenty-ui/display';
import { type ThemeColor } from 'twenty-ui/theme';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
type RatioOption = {
value: string;
label: string;
color?: ThemeColor;
};
export const ChartRatioOptionValueSelectionDropdownContent = ({
currentFieldMetadataId,
setIsOptionValueMenuOpen,
}: {
currentFieldMetadataId: string;
setIsOptionValueMenuOpen: (isOpen: boolean) => void;
}) => {
const [searchQuery, setSearchQuery] = useState('');
const { objectMetadataItems } = useObjectMetadataItems();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
if (
widgetInEditMode?.configuration?.__typename !==
'AggregateChartConfiguration'
) {
return null;
}
const sourceObjectMetadataItem = objectMetadataItems.find(
(item) => item.id === widgetInEditMode.objectMetadataId,
);
const selectedField = sourceObjectMetadataItem?.fields.find(
(field) => field.id === currentFieldMetadataId,
);
if (!isDefined(sourceObjectMetadataItem) || !isDefined(selectedField)) {
return null;
}
const getOptionsForField = (): RatioOption[] => {
if (selectedField.type === FieldMetadataType.BOOLEAN) {
return [
{ value: 'true', label: t`True` },
{ value: 'false', label: t`False` },
];
}
if (
selectedField.type === FieldMetadataType.SELECT ||
selectedField.type === FieldMetadataType.MULTI_SELECT
) {
const fieldOptions = selectedField.options ?? [];
return fieldOptions.map((option) => ({
value: option.value,
label: option.label,
color: option.color as ThemeColor,
}));
}
return [];
};
const isBoolean = selectedField.type === FieldMetadataType.BOOLEAN;
const options = getOptionsForField();
const filteredOptions = filterBySearchQuery({
items: options,
searchQuery,
getSearchableValues: (item) => [item.label],
});
return (
<>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={() => setIsOptionValueMenuOpen(false)}
Icon={IconChevronLeft}
/>
}
>
{t`Select value`}
</DropdownMenuHeader>
<DropdownMenuSearchInput
autoFocus
type="text"
placeholder={t`Search options`}
onChange={(event) => setSearchQuery(event.target.value)}
value={searchQuery}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={filteredOptions.map((item) => item.value)}
>
{filteredOptions.map((option) =>
isBoolean ? (
<ChartRatioOptionBooleanSelectableListItem
key={option.value}
optionValue={option.value}
label={option.label}
currentFieldMetadataId={currentFieldMetadataId}
/>
) : (
<ChartRatioOptionSelectSelectableListItem
key={option.value}
optionValue={option.value}
label={option.label}
color={option.color}
currentFieldMetadataId={currentFieldMetadataId}
/>
),
)}
</SelectableList>
</DropdownMenuItemsContainer>
</>
);
};
@@ -9,7 +9,7 @@ import { getFieldLabelWithSubField } from '@/command-menu/pages/page-layout/util
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
import { plural } from '@lingui/core/macro';
import { plural, t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
@@ -163,14 +163,39 @@ export const useChartSettingsValues = ({
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y:
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_AGGREGATE:
case CHART_CONFIGURATION_SETTING_IDS.EACH_SLICE_REPRESENTS: {
const hasAggregateLabel = isDefined(aggregateField?.label);
const hasAggregateOperation = isDefined(aggregateOperation);
if (
configuration.__typename === 'AggregateChartConfiguration' &&
isDefined(configuration.ratioAggregateConfig)
) {
const ratioField = objectMetadataItem?.fields.find(
(field) =>
field.id === configuration.ratioAggregateConfig?.fieldMetadataId,
);
return `${aggregateField?.label ?? ''}${
hasAggregateLabel && hasAggregateOperation
? ` (${getAggregateOperationLabel(aggregateOperation)})`
: ''
}`;
const optionValue = configuration.ratioAggregateConfig.optionValue;
const getBooleanLabel = (value: string) =>
value === 'true' ? t`True` : t`False`;
const ratioOptionLabel =
ratioField?.options?.find((option) => option.value === optionValue)
?.label ??
(ratioField?.type === 'BOOLEAN'
? getBooleanLabel(optionValue)
: capitalize(optionValue));
return [aggregateField?.label, `(${t`Ratio`}: ${ratioOptionLabel})`]
.filter(isDefined)
.join(' ');
}
const operationLabel =
isDefined(aggregateField?.label) && isDefined(aggregateOperation)
? `(${getAggregateOperationLabel(aggregateOperation)})`
: undefined;
return [aggregateField?.label, operationLabel]
.filter(isDefined)
.join(' ');
}
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_PIE_CHART: {
const pieChartGroupByField = isDefined(finalGroupByFieldYId)
@@ -98,6 +98,10 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
suffix
timezone
firstDayOfTheWeek
ratioAggregateConfig {
fieldMetadataId
optionValue
}
}
... on GaugeChartConfiguration {
graphType
@@ -0,0 +1 @@
export const DASHBOARD_AGGREGATE_OPERATION_RATIO = 'RATIO';
@@ -0,0 +1,6 @@
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { type DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
export type AggregateChartOperation =
| ExtendedAggregateOperations
| typeof DASHBOARD_AGGREGATE_OPERATION_RATIO;
@@ -0,0 +1,137 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { type RatioAggregateConfig } from '~/generated/graphql';
import { buildRatioNumeratorFilter } from '../buildRatioNumeratorFilter';
describe('buildRatioNumeratorFilter', () => {
const mockBooleanField = {
id: 'field-1',
name: 'isActive',
type: FieldMetadataType.BOOLEAN,
} as FieldMetadataItem;
const mockSelectField = {
id: 'field-2',
name: 'stage',
type: FieldMetadataType.SELECT,
} as FieldMetadataItem;
const mockMultiSelectField = {
id: 'field-3',
name: 'tags',
type: FieldMetadataType.MULTI_SELECT,
} as FieldMetadataItem;
const baseFilter = { createdAt: { gte: '2024-01-01' } };
it('should return baseFilter when ratioConfig is undefined', () => {
const result = buildRatioNumeratorFilter({
ratioConfig: undefined,
ratioField: mockBooleanField,
baseFilter,
});
expect(result).toEqual(baseFilter);
});
it('should return baseFilter when ratioField is undefined', () => {
const ratioConfig: RatioAggregateConfig = {
fieldMetadataId: 'field-1',
optionValue: 'true',
};
const result = buildRatioNumeratorFilter({
ratioConfig,
ratioField: undefined,
baseFilter,
});
expect(result).toEqual(baseFilter);
});
it('should build correct filter for BOOLEAN field with "true"', () => {
const ratioConfig: RatioAggregateConfig = {
fieldMetadataId: 'field-1',
optionValue: 'true',
};
const result = buildRatioNumeratorFilter({
ratioConfig,
ratioField: mockBooleanField,
baseFilter: undefined,
});
expect(result).toEqual({
isActive: { eq: true },
});
});
it('should build correct filter for BOOLEAN field with "false"', () => {
const ratioConfig: RatioAggregateConfig = {
fieldMetadataId: 'field-1',
optionValue: 'false',
};
const result = buildRatioNumeratorFilter({
ratioConfig,
ratioField: mockBooleanField,
baseFilter: undefined,
});
expect(result).toEqual({
isActive: { eq: false },
});
});
it('should build correct filter for SELECT field', () => {
const ratioConfig: RatioAggregateConfig = {
fieldMetadataId: 'field-2',
optionValue: 'WON',
};
const result = buildRatioNumeratorFilter({
ratioConfig,
ratioField: mockSelectField,
baseFilter: undefined,
});
expect(result).toEqual({
stage: { eq: 'WON' },
});
});
it('should build correct filter for MULTI_SELECT field using containsAny', () => {
const ratioConfig: RatioAggregateConfig = {
fieldMetadataId: 'field-3',
optionValue: 'urgent',
};
const result = buildRatioNumeratorFilter({
ratioConfig,
ratioField: mockMultiSelectField,
baseFilter: undefined,
});
expect(result).toEqual({
tags: { containsAny: ['urgent'] },
});
});
it('should combine with baseFilter using AND when both exist', () => {
const ratioConfig: RatioAggregateConfig = {
fieldMetadataId: 'field-2',
optionValue: 'WON',
};
const result = buildRatioNumeratorFilter({
ratioConfig,
ratioField: mockSelectField,
baseFilter,
});
expect(result).toEqual({
and: [baseFilter, { stage: { eq: 'WON' } }],
});
});
});
@@ -0,0 +1,57 @@
import { computeRatioDisplayValue } from '../computeRatioDisplayValue';
describe('computeRatioDisplayValue', () => {
it('should return "0%" when denominator is 0', () => {
const result = computeRatioDisplayValue({
numeratorCount: 10,
denominatorCount: 0,
});
expect(result).toBe('0%');
});
it('should return whole number without decimal when ratio is exact', () => {
const result = computeRatioDisplayValue({
numeratorCount: 50,
denominatorCount: 100,
});
expect(result).toBe('50%');
});
it('should return one decimal place when ratio has decimals', () => {
const result = computeRatioDisplayValue({
numeratorCount: 1,
denominatorCount: 3,
});
expect(result).toBe('33.3%');
});
it('should return "100%" when numerator equals denominator', () => {
const result = computeRatioDisplayValue({
numeratorCount: 100,
denominatorCount: 100,
});
expect(result).toBe('100%');
});
it('should handle small percentages', () => {
const result = computeRatioDisplayValue({
numeratorCount: 1,
denominatorCount: 200,
});
expect(result).toBe('0.5%');
});
it('should return "0%" when numerator is 0', () => {
const result = computeRatioDisplayValue({
numeratorCount: 0,
denominatorCount: 100,
});
expect(result).toBe('0%');
});
});
@@ -0,0 +1,63 @@
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { getAvailableAggregateOperationsForAggregateChart } from '../getAvailableAggregateOperationsForAggregateChart';
describe('getAvailableAggregateOperationsForAggregateChart', () => {
it('should include RATIO for BOOLEAN field', () => {
const result = getAvailableAggregateOperationsForAggregateChart({
fieldMetadataType: FieldMetadataType.BOOLEAN,
});
expect(result).toContain(DASHBOARD_AGGREGATE_OPERATION_RATIO);
});
it('should include RATIO for SELECT field', () => {
const result = getAvailableAggregateOperationsForAggregateChart({
fieldMetadataType: FieldMetadataType.SELECT,
});
expect(result).toContain(DASHBOARD_AGGREGATE_OPERATION_RATIO);
});
it('should include RATIO for MULTI_SELECT field', () => {
const result = getAvailableAggregateOperationsForAggregateChart({
fieldMetadataType: FieldMetadataType.MULTI_SELECT,
});
expect(result).toContain(DASHBOARD_AGGREGATE_OPERATION_RATIO);
});
it('should not include RATIO for NUMBER field', () => {
const result = getAvailableAggregateOperationsForAggregateChart({
fieldMetadataType: FieldMetadataType.NUMBER,
});
expect(result).not.toContain(DASHBOARD_AGGREGATE_OPERATION_RATIO);
});
it('should not include RATIO for DATE field', () => {
const result = getAvailableAggregateOperationsForAggregateChart({
fieldMetadataType: FieldMetadataType.DATE,
});
expect(result).not.toContain(DASHBOARD_AGGREGATE_OPERATION_RATIO);
});
it('should not include RATIO for TEXT field', () => {
const result = getAvailableAggregateOperationsForAggregateChart({
fieldMetadataType: FieldMetadataType.TEXT,
});
expect(result).not.toContain(DASHBOARD_AGGREGATE_OPERATION_RATIO);
});
it('should handle undefined fieldMetadataType', () => {
const result = getAvailableAggregateOperationsForAggregateChart({
fieldMetadataType: undefined,
});
expect(result).not.toContain(DASHBOARD_AGGREGATE_OPERATION_RATIO);
expect(Array.isArray(result)).toBe(true);
});
});
@@ -0,0 +1,61 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { type RatioAggregateConfig } from '~/generated/graphql';
export const buildRatioNumeratorFilter = ({
ratioConfig,
ratioField,
baseFilter,
}: {
ratioConfig: RatioAggregateConfig | null | undefined;
ratioField: FieldMetadataItem | undefined;
baseFilter: RecordGqlOperationFilter | undefined;
}): RecordGqlOperationFilter | undefined => {
if (!isDefined(ratioConfig) || !isDefined(ratioField)) {
return baseFilter;
}
const fieldFilter = buildFieldFilter(
ratioField.name,
ratioField.type,
ratioConfig.optionValue,
);
if (!isDefined(baseFilter)) {
return fieldFilter;
}
return {
and: [baseFilter, fieldFilter],
};
};
const buildFieldFilter = (
fieldName: string,
fieldType: FieldMetadataType,
optionValue: string,
): RecordGqlOperationFilter => {
if (fieldType === FieldMetadataType.BOOLEAN) {
return {
[fieldName]: {
eq: optionValue === 'true',
},
};
}
if (fieldType === FieldMetadataType.MULTI_SELECT) {
return {
[fieldName]: {
containsAny: [optionValue],
},
};
}
return {
[fieldName]: {
eq: optionValue,
},
};
};
@@ -0,0 +1,20 @@
export const computeRatioDisplayValue = ({
numeratorCount,
denominatorCount,
}: {
numeratorCount: number;
denominatorCount: number;
}): string => {
if (denominatorCount === 0) {
return '0%';
}
const ratio = (numeratorCount / denominatorCount) * 100;
const formattedRatio = ratio.toFixed(1);
if (formattedRatio.endsWith('.0')) {
return `${Math.round(ratio)}%`;
}
return `${formattedRatio}%`;
};
@@ -0,0 +1,14 @@
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { type AggregateChartOperation } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/types/AggregateChartOperation';
import { t } from '@lingui/core/macro';
export const getAggregateChartOperationLabel = (
operation: AggregateChartOperation,
) => {
if (operation === DASHBOARD_AGGREGATE_OPERATION_RATIO) {
return t`Ratio`;
}
return getAggregateOperationLabel(operation);
};
@@ -0,0 +1,25 @@
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { type AggregateChartOperation } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/types/AggregateChartOperation';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getAvailableAggregateOperationsForAggregateChart = ({
fieldMetadataType,
}: {
fieldMetadataType?: FieldMetadataType;
}): AggregateChartOperation[] => {
const baseOperations = getAvailableAggregateOperationsForFieldMetadataType({
fieldMetadataType,
});
const isSelectOrBoolean =
fieldMetadataType === FieldMetadataType.SELECT ||
fieldMetadataType === FieldMetadataType.MULTI_SELECT ||
fieldMetadataType === FieldMetadataType.BOOLEAN;
if (isSelectOrBoolean) {
return [...baseOperations, DASHBOARD_AGGREGATE_OPERATION_RATIO];
}
return baseOperations;
};
@@ -1,19 +1,20 @@
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { transformAggregateRawValueIntoAggregateDisplayValue } from '@/object-record/record-aggregate/utils/transformAggregateRawValueIntoAggregateDisplayValue';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { getRecordAggregateDisplayLabel } from '@/object-record/record-index/utils/getRecordndexAggregateDisplayLabel';
import { convertAggregateOperationToExtendedAggregateOperation } from '@/object-record/utils/convertAggregateOperationToExtendedAggregateOperation';
import { buildRatioNumeratorFilter } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/utils/buildRatioNumeratorFilter';
import { computeRatioDisplayValue } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/utils/computeRatioDisplayValue';
import { useGraphWidgetQueryCommon } from '@/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon';
import { UserContext } from '@/users/contexts/UserContext';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from 'twenty-shared/constants';
import { findById, isDefined } from 'twenty-shared/utils';
import {
AggregateOperations,
type AggregateChartConfiguration,
} from '~/generated/graphql';
import { type AggregateChartConfiguration } from '~/generated/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
export const useGraphWidgetAggregateQuery = ({
@@ -30,6 +31,20 @@ export const useGraphWidgetAggregateQuery = ({
});
const aggregateOperation = configuration.aggregateOperation;
const ratioConfig = configuration.ratioAggregateConfig;
const isRatioQuery = isDefined(ratioConfig);
const ratioField = isRatioQuery
? objectMetadataItem.fields.find(
(field) => field.id === ratioConfig.fieldMetadataId,
)
: undefined;
const ratioNumeratorFilter = buildRatioNumeratorFilter({
ratioConfig,
ratioField,
baseFilter: gqlOperationFilter,
});
const extendedAggregateOperation =
convertAggregateOperationToExtendedAggregateOperation(
@@ -43,11 +58,73 @@ export const useGraphWidgetAggregateQuery = ({
[aggregateField.name]: [extendedAggregateOperation],
},
filter: gqlOperationFilter,
skip: isRatioQuery,
});
const {
data: ratioNumeratorData,
loading: ratioNumeratorLoading,
error: ratioNumeratorError,
} = useAggregateRecords({
objectNameSingular: objectMetadataItem.nameSingular,
recordGqlFieldsAggregate: {
[FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION]: [AggregateOperations.COUNT],
},
filter: ratioNumeratorFilter,
skip: !isRatioQuery,
});
const {
data: ratioDenominatorData,
loading: ratioDenominatorLoading,
error: ratioDenominatorError,
} = useAggregateRecords({
objectNameSingular: objectMetadataItem.nameSingular,
recordGqlFieldsAggregate: {
[FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION]: [AggregateOperations.COUNT],
},
filter: gqlOperationFilter,
skip: !isRatioQuery,
});
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
const dateLocale = useRecoilValue(dateLocaleState);
if (isRatioQuery) {
const isRatioLoading = ratioNumeratorLoading || ratioDenominatorLoading;
const hasRatioData =
isDefined(ratioNumeratorData) && isDefined(ratioDenominatorData);
if (isRatioLoading || !hasRatioData) {
return {
value: '-',
label: t`Ratio`,
loading: isRatioLoading,
error: ratioNumeratorError ?? ratioDenominatorError,
};
}
const numeratorCount =
ratioNumeratorData[FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION]?.[
AggregateOperations.COUNT
] ?? 0;
const denominatorCount =
ratioDenominatorData[FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION]?.[
AggregateOperations.COUNT
] ?? 0;
return {
value: computeRatioDisplayValue({
numeratorCount: Number(numeratorCount),
denominatorCount: Number(denominatorCount),
}),
label: t`Ratio`,
loading: false,
error: ratioNumeratorError ?? ratioDenominatorError,
};
}
const aggregateFieldMetadataItem = objectMetadataItem.fields.find(
findById(configuration.aggregateFieldMetadataId),
);
@@ -0,0 +1,213 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
AggregateOperations,
GraphType,
type AggregateChartConfiguration,
} from '~/generated/graphql';
import { areChartConfigurationFieldsValidForQuery } from '../areChartConfigurationFieldsValidForQuery';
describe('areChartConfigurationFieldsValidForQuery', () => {
const createMockObjectMetadataItem = (
fields: Array<{
id: string;
type: FieldMetadataType;
options?: Array<{ value: string; label: string }>;
}>,
): ObjectMetadataItem =>
({
id: 'object-1',
nameSingular: 'opportunity',
namePlural: 'opportunities',
fields: fields.map((f) => ({
id: f.id,
name: f.id,
type: f.type,
label: f.id,
options: f.options,
})),
}) as ObjectMetadataItem;
const createAggregateConfig = (ratioAggregateConfig?: {
fieldMetadataId: string;
optionValue: string;
}): AggregateChartConfiguration => ({
__typename: 'AggregateChartConfiguration',
graphType: GraphType.AGGREGATE,
aggregateFieldMetadataId: 'aggregate-field',
aggregateOperation: AggregateOperations.COUNT,
ratioAggregateConfig: ratioAggregateConfig ?? null,
});
describe('AggregateChartConfiguration with ratioAggregateConfig', () => {
it('should return true when ratioAggregateConfig is undefined', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig(undefined),
objectMetadataItem,
);
expect(result).toBe(true);
});
it('should return false when ratio field does not exist', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig({
fieldMetadataId: 'non-existent-field',
optionValue: 'true',
}),
objectMetadataItem,
);
expect(result).toBe(false);
});
it('should return true for BOOLEAN field with "true" optionValue', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
{ id: 'boolean-field', type: FieldMetadataType.BOOLEAN },
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig({
fieldMetadataId: 'boolean-field',
optionValue: 'true',
}),
objectMetadataItem,
);
expect(result).toBe(true);
});
it('should return true for BOOLEAN field with "false" optionValue', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
{ id: 'boolean-field', type: FieldMetadataType.BOOLEAN },
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig({
fieldMetadataId: 'boolean-field',
optionValue: 'false',
}),
objectMetadataItem,
);
expect(result).toBe(true);
});
it('should return false for BOOLEAN field with invalid optionValue', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
{ id: 'boolean-field', type: FieldMetadataType.BOOLEAN },
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig({
fieldMetadataId: 'boolean-field',
optionValue: 'invalid',
}),
objectMetadataItem,
);
expect(result).toBe(false);
});
it('should return true for SELECT field with valid option', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
{
id: 'select-field',
type: FieldMetadataType.SELECT,
options: [
{ value: 'WON', label: 'Won' },
{ value: 'LOST', label: 'Lost' },
],
},
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig({
fieldMetadataId: 'select-field',
optionValue: 'WON',
}),
objectMetadataItem,
);
expect(result).toBe(true);
});
it('should return false for SELECT field with invalid option', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
{
id: 'select-field',
type: FieldMetadataType.SELECT,
options: [
{ value: 'WON', label: 'Won' },
{ value: 'LOST', label: 'Lost' },
],
},
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig({
fieldMetadataId: 'select-field',
optionValue: 'INVALID_OPTION',
}),
objectMetadataItem,
);
expect(result).toBe(false);
});
it('should return true for MULTI_SELECT field with valid option', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
{
id: 'multiselect-field',
type: FieldMetadataType.MULTI_SELECT,
options: [
{ value: 'urgent', label: 'Urgent' },
{ value: 'important', label: 'Important' },
],
},
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig({
fieldMetadataId: 'multiselect-field',
optionValue: 'urgent',
}),
objectMetadataItem,
);
expect(result).toBe(true);
});
it('should return false for unsupported field type with ratioConfig', () => {
const objectMetadataItem = createMockObjectMetadataItem([
{ id: 'aggregate-field', type: FieldMetadataType.NUMBER },
{ id: 'text-field', type: FieldMetadataType.TEXT },
]);
const result = areChartConfigurationFieldsValidForQuery(
createAggregateConfig({
fieldMetadataId: 'text-field',
optionValue: 'some-value',
}),
objectMetadataItem,
);
expect(result).toBe(false);
});
});
});
@@ -1,6 +1,10 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from 'twenty-shared/utils';
import { type PageLayoutWidget } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
type PageLayoutWidget,
type RatioAggregateConfig,
} from '~/generated/graphql';
const fieldExists = (
fieldId: string | undefined | null,
@@ -13,6 +17,39 @@ const fieldExists = (
return objectMetadataItem.fields.some((field) => field.id === fieldId);
};
const isRatioConfigValid = (
ratioConfig: RatioAggregateConfig | null | undefined,
objectMetadataItem: ObjectMetadataItem,
): boolean => {
if (!isDefined(ratioConfig)) {
return true;
}
const field = objectMetadataItem.fields.find(
(f) => f.id === ratioConfig.fieldMetadataId,
);
if (!isDefined(field)) {
return false;
}
if (field.type === FieldMetadataType.BOOLEAN) {
return (
ratioConfig.optionValue === 'true' || ratioConfig.optionValue === 'false'
);
}
if (
field.type === FieldMetadataType.SELECT ||
field.type === FieldMetadataType.MULTI_SELECT
) {
const options = field.options ?? [];
return options.some((option) => option.value === ratioConfig.optionValue);
}
return false;
};
export const areChartConfigurationFieldsValidForQuery = (
configuration: PageLayoutWidget['configuration'],
objectMetadataItem?: ObjectMetadataItem | null,
@@ -54,6 +91,17 @@ export const areChartConfigurationFieldsValidForQuery = (
);
case 'AggregateChartConfiguration':
return (
fieldExists(
configuration.aggregateFieldMetadataId,
objectMetadataItem,
) &&
isRatioConfigValid(
configuration.ratioAggregateConfig,
objectMetadataItem,
)
);
case 'GaugeChartConfiguration':
return fieldExists(
configuration.aggregateFieldMetadataId,
@@ -1,5 +1,6 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsEnum,
@@ -11,6 +12,7 @@ import {
IsUUID,
Max,
Min,
ValidateNested,
} from 'class-validator';
import { GraphQLJSON } from 'graphql-type-json';
import { CalendarStartDay } from 'twenty-shared/constants';
@@ -19,6 +21,7 @@ import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-build
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { RatioAggregateConfigDTO } from 'src/engine/metadata-modules/page-layout/dtos/ratio-aggregate-config.dto';
import { GraphType } from 'src/engine/metadata-modules/page-layout/enums/graph-type.enum';
@ObjectType('AggregateChartConfiguration')
@@ -83,4 +86,10 @@ export class AggregateChartConfigurationDTO {
@IsString()
@IsOptional()
suffix?: string;
@Field(() => RatioAggregateConfigDTO, { nullable: true })
@ValidateNested()
@Type(() => RatioAggregateConfigDTO)
@IsOptional()
ratioAggregateConfig?: RatioAggregateConfigDTO;
}
@@ -0,0 +1,19 @@
import { Field, InputType, ObjectType } from '@nestjs/graphql';
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('RatioAggregateConfig')
@InputType('RatioAggregateConfigInput')
export class RatioAggregateConfigDTO {
@Field(() => UUIDScalarType)
@IsUUID()
@IsNotEmpty()
fieldMetadataId: string;
@Field(() => String)
@IsString()
@IsNotEmpty()
optionValue: string;
}