[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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
+91
@@ -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>
|
||||
);
|
||||
};
|
||||
+49
-69
@@ -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>
|
||||
</>
|
||||
|
||||
+1
@@ -101,6 +101,7 @@ export const ChartDataSourceDropdownContent = () => {
|
||||
groupByFieldMetadataId: null,
|
||||
groupBySubFieldName: null,
|
||||
filter: {},
|
||||
ratioAggregateConfig: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+52
@@ -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>
|
||||
);
|
||||
};
|
||||
+72
@@ -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>
|
||||
);
|
||||
};
|
||||
+74
@@ -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>
|
||||
);
|
||||
};
|
||||
+143
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+33
-8
@@ -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)
|
||||
|
||||
+4
@@ -98,6 +98,10 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
suffix
|
||||
timezone
|
||||
firstDayOfTheWeek
|
||||
ratioAggregateConfig {
|
||||
fieldMetadataId
|
||||
optionValue
|
||||
}
|
||||
}
|
||||
... on GaugeChartConfiguration {
|
||||
graphType
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const DASHBOARD_AGGREGATE_OPERATION_RATIO = 'RATIO';
|
||||
+6
@@ -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;
|
||||
+137
@@ -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' } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
+57
@@ -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%');
|
||||
});
|
||||
});
|
||||
+63
@@ -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);
|
||||
});
|
||||
});
|
||||
+61
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
+20
@@ -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}%`;
|
||||
};
|
||||
+14
@@ -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);
|
||||
};
|
||||
+25
@@ -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;
|
||||
};
|
||||
+81
-4
@@ -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),
|
||||
);
|
||||
|
||||
+213
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+49
-1
@@ -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,
|
||||
|
||||
+9
@@ -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;
|
||||
}
|
||||
|
||||
+19
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user