[DASHBOARDS] Manual and position-based sorting for chart widgets (#16794)
## Description SELECT fields have a defined option order that users expect to see reflected in charts. This PR allows sorting by that position and also enables custom manual ordering. ## Video QA ### Reordering on primary axis https://github.com/user-attachments/assets/994f515e-19cb-4a5e-b745-e8c77e92ae0b ### Reordering on secondary axis https://github.com/user-attachments/assets/444c16f2-1920-4dc4-8b42-312d520ab43b Note: The colors in the graph will match the colors of the select options, but this will be done in another PR <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces new sort modes and UI for chart groupings, with full FE/BE support and updated GraphQL schema. > > - Extend `GraphOrderBy` with `FIELD_POSITION_ASC/DESC` and `MANUAL`; add corresponding fields in configs: `primaryAxisManualSortOrder`, `secondaryAxisManualSortOrder`, and `manualSortOrder` (pie) > - New UI: dropdown options filtered by field type, icons, and a draggable submenu (`ChartManualSortSubMenuContent`) to reorder select options; integrates with widget edit flow > - Sorting logic added/refactored: `sortChartData`, `sortByManualOrder`, `sortBySelectOptionPosition`, `sortLineChartSeries`, plus updates to bar/line/pie transformers to honor new modes and manual orders > - Default behaviors: select fields default to `FIELD_POSITION_ASC`; query variable builders skip `orderBy` when using manual/position sorts > - Update GraphQL generated types/fragments/queries and backend DTOs/schemas to persist new fields; add tests for sorting utilities and snapshots; add sorting icons in `twenty-ui` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 78c9b56c0f1f2d45f7f8b270bb59ca599a005abe. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@@ -362,12 +362,14 @@ export type BarChartConfiguration = {
|
||||
primaryAxisDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
|
||||
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
|
||||
primaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
primaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
primaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
rangeMax?: Maybe<Scalars['Float']>;
|
||||
rangeMin?: Maybe<Scalars['Float']>;
|
||||
secondaryAxisGroupByDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
|
||||
secondaryAxisGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
|
||||
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
secondaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
};
|
||||
@@ -1559,6 +1561,9 @@ export type GetWebhookInput = {
|
||||
export enum GraphOrderBy {
|
||||
FIELD_ASC = 'FIELD_ASC',
|
||||
FIELD_DESC = 'FIELD_DESC',
|
||||
FIELD_POSITION_ASC = 'FIELD_POSITION_ASC',
|
||||
FIELD_POSITION_DESC = 'FIELD_POSITION_DESC',
|
||||
MANUAL = 'MANUAL',
|
||||
VALUE_ASC = 'VALUE_ASC',
|
||||
VALUE_DESC = 'VALUE_DESC'
|
||||
}
|
||||
@@ -1767,12 +1772,14 @@ export type LineChartConfiguration = {
|
||||
primaryAxisDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
|
||||
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
|
||||
primaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
primaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
primaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
rangeMax?: Maybe<Scalars['Float']>;
|
||||
rangeMin?: Maybe<Scalars['Float']>;
|
||||
secondaryAxisGroupByDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
|
||||
secondaryAxisGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
|
||||
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
secondaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
};
|
||||
@@ -3288,6 +3295,7 @@ export type PieChartConfiguration = {
|
||||
groupByFieldMetadataId: Scalars['UUID'];
|
||||
groupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
hideEmptyCategory?: Maybe<Scalars['Boolean']>;
|
||||
manualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
orderBy?: Maybe<GraphOrderBy>;
|
||||
showCenterMetric?: Maybe<Scalars['Boolean']>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
|
||||
@@ -362,12 +362,14 @@ export type BarChartConfiguration = {
|
||||
primaryAxisDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
|
||||
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
|
||||
primaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
primaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
primaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
rangeMax?: Maybe<Scalars['Float']>;
|
||||
rangeMin?: Maybe<Scalars['Float']>;
|
||||
secondaryAxisGroupByDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
|
||||
secondaryAxisGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
|
||||
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
secondaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
};
|
||||
@@ -1527,6 +1529,9 @@ export type GetWebhookInput = {
|
||||
export enum GraphOrderBy {
|
||||
FIELD_ASC = 'FIELD_ASC',
|
||||
FIELD_DESC = 'FIELD_DESC',
|
||||
FIELD_POSITION_ASC = 'FIELD_POSITION_ASC',
|
||||
FIELD_POSITION_DESC = 'FIELD_POSITION_DESC',
|
||||
MANUAL = 'MANUAL',
|
||||
VALUE_ASC = 'VALUE_ASC',
|
||||
VALUE_DESC = 'VALUE_DESC'
|
||||
}
|
||||
@@ -1735,12 +1740,14 @@ export type LineChartConfiguration = {
|
||||
primaryAxisDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
|
||||
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
|
||||
primaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
primaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
primaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
rangeMax?: Maybe<Scalars['Float']>;
|
||||
rangeMin?: Maybe<Scalars['Float']>;
|
||||
secondaryAxisGroupByDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
|
||||
secondaryAxisGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
|
||||
secondaryAxisGroupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
secondaryAxisManualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
secondaryAxisOrderBy?: Maybe<GraphOrderBy>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
};
|
||||
@@ -3171,6 +3178,7 @@ export type PieChartConfiguration = {
|
||||
groupByFieldMetadataId: Scalars['UUID'];
|
||||
groupBySubFieldName?: Maybe<Scalars['String']>;
|
||||
hideEmptyCategory?: Maybe<Scalars['Boolean']>;
|
||||
manualSortOrder?: Maybe<Array<Scalars['String']>>;
|
||||
orderBy?: Maybe<GraphOrderBy>;
|
||||
showCenterMetric?: Maybe<Scalars['Boolean']>;
|
||||
timezone?: Maybe<Scalars['String']>;
|
||||
@@ -5082,7 +5090,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', configurationType: WidgetConfigurationType, 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', configurationType: WidgetConfigurationType, 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, layout: BarChartLayout, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, 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', configurationType: WidgetConfigurationType, 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', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: 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', configurationType: WidgetConfigurationType, 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', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array<string> | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array<string> | 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, layout: BarChartLayout, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array<string> | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array<string> | 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', configurationType: WidgetConfigurationType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array<string> | 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', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } };
|
||||
|
||||
export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
@@ -5090,7 +5098,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', configurationType: WidgetConfigurationType, 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', configurationType: WidgetConfigurationType, 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, layout: BarChartLayout, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, 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', configurationType: WidgetConfigurationType, 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', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | 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', configurationType: WidgetConfigurationType, 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', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array<string> | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array<string> | 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, layout: BarChartLayout, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array<string> | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array<string> | 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', configurationType: WidgetConfigurationType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array<string> | 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', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } }> | null }> | null } };
|
||||
|
||||
export type OnDbEventSubscriptionVariables = Exact<{
|
||||
input: OnDbEventInput;
|
||||
@@ -5416,10 +5424,12 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
primaryAxisGroupBySubFieldName
|
||||
primaryAxisDateGranularity
|
||||
primaryAxisOrderBy
|
||||
primaryAxisManualSortOrder
|
||||
secondaryAxisGroupByFieldMetadataId
|
||||
secondaryAxisGroupBySubFieldName
|
||||
secondaryAxisGroupByDateGranularity
|
||||
secondaryAxisOrderBy
|
||||
secondaryAxisManualSortOrder
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
@@ -5443,10 +5453,12 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
primaryAxisGroupBySubFieldName
|
||||
primaryAxisDateGranularity
|
||||
primaryAxisOrderBy
|
||||
primaryAxisManualSortOrder
|
||||
secondaryAxisGroupByFieldMetadataId
|
||||
secondaryAxisGroupBySubFieldName
|
||||
secondaryAxisGroupByDateGranularity
|
||||
secondaryAxisOrderBy
|
||||
secondaryAxisManualSortOrder
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
@@ -5469,6 +5481,7 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql`
|
||||
groupBySubFieldName
|
||||
dateGranularity
|
||||
orderBy
|
||||
manualSortOrder
|
||||
displayDataLabel
|
||||
showCenterMetric
|
||||
displayLegend
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
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 { getManualSortOrderFromConfig } from '@/command-menu/pages/page-layout/utils/getManualSortOrderFromConfig';
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { sortOptionsForManualOrder } from '@/page-layout/widgets/graph/utils/sortOptionsForManualOrder';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
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 { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { MenuItemDraggable } from 'twenty-ui/navigation';
|
||||
import { type WidgetConfiguration } from '~/generated/graphql';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
|
||||
type ChartManualSortSubMenuContentProps = {
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
axis: 'primary' | 'secondary';
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const ChartManualSortSubMenuContent = ({
|
||||
fieldMetadataItem,
|
||||
axis,
|
||||
onBack,
|
||||
}: ChartManualSortSubMenuContentProps) => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
// TODO: Remove this cast when FieldsConfiguration and FieldConfiguration are in the backend
|
||||
const configuration = widgetInEditMode?.configuration as WidgetConfiguration;
|
||||
const options = fieldMetadataItem.options ?? [];
|
||||
|
||||
const currentManualSortOrder = getManualSortOrderFromConfig(
|
||||
configuration,
|
||||
axis,
|
||||
);
|
||||
|
||||
const sortedOptions = sortOptionsForManualOrder(
|
||||
options,
|
||||
currentManualSortOrder,
|
||||
);
|
||||
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
if (!isDefined(result.destination)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedOptions = moveArrayItem(sortedOptions, {
|
||||
fromIndex: result.source.index,
|
||||
toIndex: result.destination.index,
|
||||
});
|
||||
|
||||
const newManualSortOrder = reorderedOptions.map((option) => option.value);
|
||||
const configKey = isWidgetConfigurationOfType(
|
||||
configuration,
|
||||
'PieChartConfiguration',
|
||||
)
|
||||
? 'manualSortOrder'
|
||||
: axis === 'primary'
|
||||
? 'primaryAxisManualSortOrder'
|
||||
: 'secondaryAxisManualSortOrder';
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: { [configKey]: newManualSortOrder },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={onBack}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t`Reorder options`}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<DraggableList
|
||||
onDragEnd={handleDragEnd}
|
||||
draggableItems={
|
||||
<>
|
||||
{sortedOptions.map((option, index) => (
|
||||
<DraggableItem
|
||||
key={option.value}
|
||||
draggableId={option.value}
|
||||
index={index}
|
||||
isDragDisabled={sortedOptions.length === 1}
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
showGrip
|
||||
isDragDisabled={sortedOptions.length === 1}
|
||||
text={
|
||||
<Tag
|
||||
preventShrink
|
||||
color={option.color}
|
||||
text={option.label}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+92
-27
@@ -1,9 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ChartManualSortSubMenuContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartManualSortSubMenuContent';
|
||||
import { AGGREGATE_SORT_BY_OPTIONS } from '@/command-menu/pages/page-layout/constants/AggregateSortByOptions';
|
||||
import { useGraphGroupBySortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphGroupBySortOptionLabels';
|
||||
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 { filterSortOptionsByFieldType } from '@/command-menu/pages/page-layout/utils/filterSortOptionsByFieldType';
|
||||
import { getDefaultManualSortOrder } from '@/command-menu/pages/page-layout/utils/getDefaultManualSortOrder';
|
||||
import { getSortIconForFieldType } from '@/command-menu/pages/page-layout/utils/getSortIconForFieldType';
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
@@ -13,13 +20,20 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
|
||||
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';
|
||||
import { type GraphOrderBy } from '~/generated/graphql';
|
||||
import {
|
||||
GraphOrderBy,
|
||||
type GraphOrderBy as GraphOrderByType,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const ChartSortByGroupByFieldDropdownContent = () => {
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
|
||||
@@ -34,6 +48,7 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
|
||||
if (!isDefined(widgetInEditMode?.objectMetadataId)) {
|
||||
throw new Error('No data source in chart');
|
||||
}
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
@@ -43,33 +58,76 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleSelectSortOption = (orderBy: GraphOrderBy) => {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: { secondaryAxisOrderBy: orderBy },
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
const secondaryAxisField = objectMetadataItem?.fields.find(
|
||||
(field) => field.id === configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
);
|
||||
|
||||
const { getGroupBySortOptionLabel } = useGraphGroupBySortOptionLabels({
|
||||
objectMetadataId: widgetInEditMode.objectMetadataId,
|
||||
});
|
||||
|
||||
if (!isDefined(secondaryAxisField)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectSortOption = (orderBy: GraphOrderByType) => {
|
||||
const configToUpdate: Record<string, unknown> = {
|
||||
secondaryAxisOrderBy: orderBy,
|
||||
};
|
||||
|
||||
if (orderBy === GraphOrderBy.MANUAL) {
|
||||
const existingManualSortOrder =
|
||||
configuration.secondaryAxisManualSortOrder;
|
||||
|
||||
if (!isDefined(existingManualSortOrder)) {
|
||||
configToUpdate.secondaryAxisManualSortOrder = getDefaultManualSortOrder(
|
||||
secondaryAxisField?.options,
|
||||
);
|
||||
}
|
||||
|
||||
updateCurrentWidgetConfig({ configToUpdate });
|
||||
setIsSubMenuOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (configuration.secondaryAxisOrderBy === GraphOrderBy.MANUAL) {
|
||||
configToUpdate.secondaryAxisManualSortOrder = null;
|
||||
}
|
||||
|
||||
updateCurrentWidgetConfig({ configToUpdate });
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const availableOptions = filterSortOptionsByFieldType({
|
||||
options: AGGREGATE_SORT_BY_OPTIONS,
|
||||
fieldType: secondaryAxisField?.type,
|
||||
});
|
||||
|
||||
if (isSubMenuOpen && isDefined(secondaryAxisField)) {
|
||||
return (
|
||||
<ChartManualSortSubMenuContent
|
||||
fieldMetadataItem={secondaryAxisField}
|
||||
axis="secondary"
|
||||
onBack={() => setIsSubMenuOpen(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={AGGREGATE_SORT_BY_OPTIONS.map(
|
||||
(option) => option.value,
|
||||
)}
|
||||
>
|
||||
{AGGREGATE_SORT_BY_OPTIONS.map((sortOption) => (
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableOptions.map((option) => option.value)}
|
||||
>
|
||||
{availableOptions.map((sortOption) => {
|
||||
const isManualOption = sortOption.value === GraphOrderBy.MANUAL;
|
||||
|
||||
return (
|
||||
<SelectableListItem
|
||||
key={sortOption.value}
|
||||
itemId={sortOption.value}
|
||||
@@ -87,15 +145,22 @@ export const ChartSortByGroupByFieldDropdownContent = () => {
|
||||
configuration.secondaryAxisOrderBy === sortOption.value
|
||||
}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={sortOption.icon}
|
||||
LeftIcon={
|
||||
sortOption.icon ??
|
||||
getSortIconForFieldType({
|
||||
fieldType: secondaryAxisField?.type,
|
||||
orderBy: sortOption.value,
|
||||
})
|
||||
}
|
||||
hasSubMenu={isManualOption}
|
||||
onClick={() => {
|
||||
handleSelectSortOption(sortOption.value);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+115
-74
@@ -1,11 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ChartManualSortSubMenuContent } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartManualSortSubMenuContent';
|
||||
import { X_SORT_BY_OPTIONS } from '@/command-menu/pages/page-layout/constants/XSortByOptions';
|
||||
import { useGraphXSortOptionLabels } from '@/command-menu/pages/page-layout/hooks/useGraphXSortOptionLabels';
|
||||
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 { filterSortOptionsByFieldType } from '@/command-menu/pages/page-layout/utils/filterSortOptionsByFieldType';
|
||||
import { getDefaultManualSortOrder } from '@/command-menu/pages/page-layout/utils/getDefaultManualSortOrder';
|
||||
import { getSortIconForFieldType } from '@/command-menu/pages/page-layout/utils/getSortIconForFieldType';
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
@@ -15,7 +20,7 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
@@ -24,8 +29,23 @@ import {
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const ChartSortBySelectionDropdownContent = () => {
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const configuration = widgetInEditMode?.configuration;
|
||||
|
||||
const isPieChart = isWidgetConfigurationOfType(
|
||||
@@ -49,25 +69,10 @@ export const ChartSortBySelectionDropdownContent = () => {
|
||||
throw new Error('No data source in chart');
|
||||
}
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useRecoilComponentValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { getXSortOptionLabel } = useGraphXSortOptionLabels({
|
||||
objectMetadataId: widgetInEditMode.objectMetadataId,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widgetInEditMode.objectMetadataId,
|
||||
);
|
||||
@@ -96,44 +101,69 @@ export const ChartSortBySelectionDropdownContent = () => {
|
||||
(field) => field.id === groupByFieldMetadataId,
|
||||
);
|
||||
|
||||
const isPrimaryAxisDateField =
|
||||
isFieldMetadataDateKind(primaryAxisField?.type) ||
|
||||
(isDefined(primaryAxisField) &&
|
||||
isRelationNestedFieldDateKind({
|
||||
relationField: primaryAxisField,
|
||||
relationNestedFieldName: groupBySubFieldName ?? undefined,
|
||||
objectMetadataItems,
|
||||
}));
|
||||
if (!isDefined(primaryAxisField)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingManualSortOrder = isPieChart
|
||||
? configuration.manualSortOrder
|
||||
: (configuration as BarChartConfiguration | LineChartConfiguration)
|
||||
.primaryAxisManualSortOrder;
|
||||
|
||||
const handleSelect = (orderBy: GraphOrderBy) => {
|
||||
if (isPieChart) {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: { orderBy },
|
||||
});
|
||||
} else {
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: { primaryAxisOrderBy: orderBy },
|
||||
});
|
||||
const configToUpdate: Record<string, unknown> = {};
|
||||
|
||||
if (orderBy === GraphOrderBy.MANUAL) {
|
||||
const orderByKey = isPieChart ? 'orderBy' : 'primaryAxisOrderBy';
|
||||
const manualSortOrderKey = isPieChart
|
||||
? 'manualSortOrder'
|
||||
: 'primaryAxisManualSortOrder';
|
||||
|
||||
configToUpdate[orderByKey] = orderBy;
|
||||
|
||||
if (!isDefined(existingManualSortOrder)) {
|
||||
configToUpdate[manualSortOrderKey] = getDefaultManualSortOrder(
|
||||
primaryAxisField?.options,
|
||||
);
|
||||
}
|
||||
|
||||
updateCurrentWidgetConfig({ configToUpdate });
|
||||
setIsSubMenuOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentOrderBy === GraphOrderBy.MANUAL) {
|
||||
const manualSortOrderKey = isPieChart
|
||||
? 'manualSortOrder'
|
||||
: 'primaryAxisManualSortOrder';
|
||||
configToUpdate[manualSortOrderKey] = null;
|
||||
}
|
||||
|
||||
if (isPieChart) {
|
||||
configToUpdate.orderBy = orderBy;
|
||||
} else {
|
||||
configToUpdate.primaryAxisOrderBy = orderBy;
|
||||
}
|
||||
|
||||
updateCurrentWidgetConfig({ configToUpdate });
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const availableOptions = X_SORT_BY_OPTIONS.filter((option) => {
|
||||
const isValueSort =
|
||||
option.value === GraphOrderBy.VALUE_ASC ||
|
||||
option.value === GraphOrderBy.VALUE_DESC;
|
||||
|
||||
if (isLineChart) {
|
||||
return !isValueSort;
|
||||
}
|
||||
|
||||
if ((isBarChart || isPieChart) && isPrimaryAxisDateField) {
|
||||
return !isValueSort;
|
||||
}
|
||||
|
||||
return true;
|
||||
const availableOptions = filterSortOptionsByFieldType({
|
||||
options: X_SORT_BY_OPTIONS,
|
||||
fieldType: primaryAxisField?.type,
|
||||
});
|
||||
|
||||
if (isSubMenuOpen && isDefined(primaryAxisField)) {
|
||||
return (
|
||||
<ChartManualSortSubMenuContent
|
||||
fieldMetadataItem={primaryAxisField}
|
||||
axis={'primary'}
|
||||
onBack={() => setIsSubMenuOpen(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
@@ -141,35 +171,46 @@ export const ChartSortBySelectionDropdownContent = () => {
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={availableOptions.map((option) => option.value)}
|
||||
>
|
||||
{availableOptions.map((sortOption) => (
|
||||
<SelectableListItem
|
||||
key={sortOption.value}
|
||||
itemId={sortOption.value}
|
||||
onEnter={() => {
|
||||
handleSelect(sortOption.value);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getXSortOptionLabel({
|
||||
graphOrderBy: sortOption.value,
|
||||
groupByFieldMetadataIdX: groupByFieldMetadataId ?? '',
|
||||
groupBySubFieldNameX: groupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined,
|
||||
aggregateFieldMetadataId:
|
||||
configuration.aggregateFieldMetadataId ?? undefined,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation ?? undefined,
|
||||
})}
|
||||
selected={currentOrderBy === sortOption.value}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={sortOption.icon}
|
||||
onClick={() => {
|
||||
{availableOptions.map((sortOption) => {
|
||||
const isManualOption = sortOption.value === GraphOrderBy.MANUAL;
|
||||
|
||||
return (
|
||||
<SelectableListItem
|
||||
key={sortOption.value}
|
||||
itemId={sortOption.value}
|
||||
onEnter={() => {
|
||||
handleSelect(sortOption.value);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={getXSortOptionLabel({
|
||||
graphOrderBy: sortOption.value,
|
||||
groupByFieldMetadataIdX: groupByFieldMetadataId ?? '',
|
||||
groupBySubFieldNameX: groupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined,
|
||||
aggregateFieldMetadataId:
|
||||
configuration.aggregateFieldMetadataId ?? undefined,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation ?? undefined,
|
||||
})}
|
||||
selected={currentOrderBy === sortOption.value}
|
||||
focused={selectedItemId === sortOption.value}
|
||||
LeftIcon={
|
||||
sortOption.icon ??
|
||||
getSortIconForFieldType({
|
||||
fieldType: primaryAxisField?.type,
|
||||
orderBy: sortOption.value,
|
||||
})
|
||||
}
|
||||
hasSubMenu={isManualOption}
|
||||
onClick={() => {
|
||||
handleSelect(sortOption.value);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
})}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
|
||||
+19
-3
@@ -1,13 +1,29 @@
|
||||
import { IconArrowDown, IconArrowUp } from 'twenty-ui/display';
|
||||
import {
|
||||
IconHandMove,
|
||||
IconSortAscending,
|
||||
IconSortDescending,
|
||||
} from 'twenty-ui/display';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const AGGREGATE_SORT_BY_OPTIONS = [
|
||||
{
|
||||
value: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
icon: IconSortAscending,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.FIELD_POSITION_DESC,
|
||||
icon: IconSortDescending,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.FIELD_ASC,
|
||||
icon: IconArrowUp,
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.FIELD_DESC,
|
||||
icon: IconArrowDown,
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.MANUAL,
|
||||
icon: IconHandMove,
|
||||
},
|
||||
];
|
||||
|
||||
+24
-5
@@ -1,19 +1,34 @@
|
||||
import {
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
type IconComponent,
|
||||
IconHandMove,
|
||||
IconSortAscending,
|
||||
IconSortDescending,
|
||||
IconTrendingDown,
|
||||
IconTrendingUp,
|
||||
} from 'twenty-ui/display';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const X_SORT_BY_OPTIONS = [
|
||||
type XSortByOption = {
|
||||
value: GraphOrderBy;
|
||||
icon: IconComponent | null;
|
||||
};
|
||||
|
||||
export const X_SORT_BY_OPTIONS: XSortByOption[] = [
|
||||
{
|
||||
value: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
icon: IconSortAscending,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.FIELD_POSITION_DESC,
|
||||
icon: IconSortDescending,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.FIELD_ASC,
|
||||
icon: IconArrowUp,
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.FIELD_DESC,
|
||||
icon: IconArrowDown,
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.VALUE_ASC,
|
||||
@@ -23,4 +38,8 @@ export const X_SORT_BY_OPTIONS = [
|
||||
value: GraphOrderBy.VALUE_DESC,
|
||||
icon: IconTrendingDown,
|
||||
},
|
||||
{
|
||||
value: GraphOrderBy.MANUAL,
|
||||
icon: IconHandMove,
|
||||
},
|
||||
];
|
||||
|
||||
+16
-3
@@ -1,8 +1,10 @@
|
||||
import { getFieldLabelWithSubField } from '@/command-menu/pages/page-layout/utils/getFieldLabelWithSubField';
|
||||
import { getSortLabelSuffixForFieldType } from '@/command-menu/pages/page-layout/utils/getSortLabelSuffixForFieldType';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const useGraphGroupBySortOptionLabels = ({
|
||||
@@ -35,13 +37,24 @@ export const useGraphGroupBySortOptionLabels = ({
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
const groupBySortLabelSuffix = getSortLabelSuffixForFieldType({
|
||||
fieldType: field?.type,
|
||||
orderBy: graphOrderBy,
|
||||
});
|
||||
|
||||
switch (graphOrderBy) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return `${fieldLabel} ${t`Ascending`}`;
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return `${fieldLabel} ${t`Descending`}`;
|
||||
default:
|
||||
case GraphOrderBy.FIELD_POSITION_ASC:
|
||||
case GraphOrderBy.FIELD_POSITION_DESC:
|
||||
return `${fieldLabel} ${groupBySortLabelSuffix}`;
|
||||
case GraphOrderBy.VALUE_ASC:
|
||||
case GraphOrderBy.VALUE_DESC:
|
||||
return '';
|
||||
case GraphOrderBy.MANUAL:
|
||||
return t`Manual`;
|
||||
default:
|
||||
assertUnreachable(graphOrderBy);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+17
-4
@@ -1,4 +1,5 @@
|
||||
import { getFieldLabelWithSubField } from '@/command-menu/pages/page-layout/utils/getFieldLabelWithSubField';
|
||||
import { getSortLabelSuffixForFieldType } from '@/command-menu/pages/page-layout/utils/getSortLabelSuffixForFieldType';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
@@ -53,15 +54,27 @@ export const useGraphXSortOptionLabels = ({
|
||||
? getAggregateOperationLabel(aggregateOperation)
|
||||
: t`Value`;
|
||||
|
||||
const groupBySortLabelSuffix = getSortLabelSuffixForFieldType({
|
||||
fieldType: groupByField?.type,
|
||||
orderBy: graphOrderBy,
|
||||
});
|
||||
|
||||
const aggregateSortLabelSuffix = getSortLabelSuffixForFieldType({
|
||||
fieldType: aggregateField?.type,
|
||||
orderBy: graphOrderBy,
|
||||
});
|
||||
|
||||
switch (graphOrderBy) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return `${fieldLabel} ${t`Ascending`}`;
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return `${fieldLabel} ${t`Descending`}`;
|
||||
case GraphOrderBy.FIELD_POSITION_ASC:
|
||||
case GraphOrderBy.FIELD_POSITION_DESC:
|
||||
return `${fieldLabel} ${groupBySortLabelSuffix}`;
|
||||
case GraphOrderBy.VALUE_ASC:
|
||||
return `${valueLabel} ${t`Ascending`}`;
|
||||
case GraphOrderBy.VALUE_DESC:
|
||||
return `${valueLabel} ${t`Descending`}`;
|
||||
return `${valueLabel} ${aggregateSortLabelSuffix}`;
|
||||
case GraphOrderBy.MANUAL:
|
||||
return t`Manual`;
|
||||
default:
|
||||
assertUnreachable(graphOrderBy);
|
||||
}
|
||||
|
||||
+19
-1
@@ -1,12 +1,12 @@
|
||||
import { type TypedBarChartConfiguration } from '@/command-menu/pages/page-layout/types/TypedBarChartConfiguration';
|
||||
import { type TypedPieChartConfiguration } from '@/command-menu/pages/page-layout/types/TypedPieChartConfiguration';
|
||||
import { buildChartGroupByFieldConfigUpdate } from '@/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate';
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import {
|
||||
BarChartGroupMode,
|
||||
GraphOrderBy,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated/graphql';
|
||||
import { buildChartGroupByFieldConfigUpdate } from '@/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate';
|
||||
|
||||
describe('buildChartGroupByFieldConfigUpdate', () => {
|
||||
it('sets default orderBy and dateGranularity for primary axis', () => {
|
||||
@@ -79,4 +79,22 @@ describe('buildChartGroupByFieldConfigUpdate', () => {
|
||||
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
|
||||
});
|
||||
});
|
||||
|
||||
it('resets orderBy to default when field changes', () => {
|
||||
const result = buildChartGroupByFieldConfigUpdate({
|
||||
configuration: {
|
||||
__typename: 'BarChartConfiguration',
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
|
||||
} as TypedBarChartConfiguration,
|
||||
fieldMetadataIdKey: 'primaryAxisGroupByFieldMetadataId',
|
||||
subFieldNameKey: 'primaryAxisGroupBySubFieldName',
|
||||
fieldId: 'new-field-id',
|
||||
subFieldName: null,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { filterSortOptionsByFieldType } from '@/command-menu/pages/page-layout/utils/filterSortOptionsByFieldType';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
describe('filterSortOptionsByFieldType', () => {
|
||||
const allOptions = [
|
||||
{ value: GraphOrderBy.FIELD_POSITION_ASC },
|
||||
{ value: GraphOrderBy.FIELD_POSITION_DESC },
|
||||
{ value: GraphOrderBy.FIELD_ASC },
|
||||
{ value: GraphOrderBy.FIELD_DESC },
|
||||
{ value: GraphOrderBy.VALUE_ASC },
|
||||
{ value: GraphOrderBy.VALUE_DESC },
|
||||
{ value: GraphOrderBy.MANUAL },
|
||||
];
|
||||
|
||||
describe('select field', () => {
|
||||
it('should include all options for select field', () => {
|
||||
const result = filterSortOptionsByFieldType({
|
||||
options: allOptions,
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('date field', () => {
|
||||
it('should exclude value sorts for date select field', () => {
|
||||
const result = filterSortOptionsByFieldType({
|
||||
options: allOptions,
|
||||
fieldType: FieldMetadataType.DATE,
|
||||
});
|
||||
|
||||
expect(result).not.toContainEqual({ value: GraphOrderBy.VALUE_ASC });
|
||||
expect(result).not.toContainEqual({ value: GraphOrderBy.VALUE_DESC });
|
||||
expect(result).not.toContainEqual({ value: GraphOrderBy.MANUAL });
|
||||
expect(result).not.toContainEqual({
|
||||
value: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
});
|
||||
expect(result).not.toContainEqual({
|
||||
value: GraphOrderBy.FIELD_POSITION_DESC,
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-select, non-date field', () => {
|
||||
it('should exclude manual and position sorts', () => {
|
||||
const result = filterSortOptionsByFieldType({
|
||||
options: allOptions,
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).not.toContainEqual({ value: GraphOrderBy.MANUAL });
|
||||
expect(result).not.toContainEqual({
|
||||
value: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
});
|
||||
expect(result).not.toContainEqual({
|
||||
value: GraphOrderBy.FIELD_POSITION_DESC,
|
||||
});
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { getChartDefaultOrderByForFieldType } from '@/command-menu/pages/page-layout/utils/getChartDefaultOrderByForFieldType';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
describe('getChartDefaultOrderByForFieldType', () => {
|
||||
it('should return FIELD_POSITION_ASC for SELECT field type', () => {
|
||||
expect(getChartDefaultOrderByForFieldType(FieldMetadataType.SELECT)).toBe(
|
||||
GraphOrderBy.FIELD_POSITION_ASC,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return FIELD_ASC for TEXT field type', () => {
|
||||
expect(getChartDefaultOrderByForFieldType(FieldMetadataType.TEXT)).toBe(
|
||||
GraphOrderBy.FIELD_ASC,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return FIELD_ASC for NUMBER field type', () => {
|
||||
expect(getChartDefaultOrderByForFieldType(FieldMetadataType.NUMBER)).toBe(
|
||||
GraphOrderBy.FIELD_ASC,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return FIELD_ASC for DATE field type', () => {
|
||||
expect(getChartDefaultOrderByForFieldType(FieldMetadataType.DATE)).toBe(
|
||||
GraphOrderBy.FIELD_ASC,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return FIELD_ASC for MULTI_SELECT field type', () => {
|
||||
expect(
|
||||
getChartDefaultOrderByForFieldType(FieldMetadataType.MULTI_SELECT),
|
||||
).toBe(GraphOrderBy.FIELD_ASC);
|
||||
});
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { getDefaultManualSortOrder } from '@/command-menu/pages/page-layout/utils/getDefaultManualSortOrder';
|
||||
|
||||
describe('getDefaultManualSortOrder', () => {
|
||||
it('should return empty array for null options', () => {
|
||||
expect(getDefaultManualSortOrder(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for undefined options', () => {
|
||||
expect(getDefaultManualSortOrder(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for empty options', () => {
|
||||
expect(getDefaultManualSortOrder([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return values sorted by position', () => {
|
||||
const options = [
|
||||
{ value: 'third', position: 2 },
|
||||
{ value: 'first', position: 0 },
|
||||
{ value: 'second', position: 1 },
|
||||
];
|
||||
|
||||
expect(getDefaultManualSortOrder(options)).toEqual([
|
||||
'first',
|
||||
'second',
|
||||
'third',
|
||||
]);
|
||||
});
|
||||
});
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { getManualSortOrderFromConfig } from '@/command-menu/pages/page-layout/utils/getManualSortOrderFromConfig';
|
||||
import { expect } from '@storybook/test';
|
||||
import {
|
||||
WidgetConfigurationType,
|
||||
type BarChartConfiguration,
|
||||
type LineChartConfiguration,
|
||||
type PieChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
describe('getManualSortOrderFromConfig', () => {
|
||||
describe('pie chart configuration', () => {
|
||||
it('should return manualSortOrder for pie axis', () => {
|
||||
const config = {
|
||||
__typename: 'PieChartConfiguration' as const,
|
||||
manualSortOrder: ['a', 'b', 'c'],
|
||||
} as PieChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should return undefined for null manualSortOrder', () => {
|
||||
const config = {
|
||||
__typename: 'PieChartConfiguration' as const,
|
||||
manualSortOrder: null,
|
||||
} as PieChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when manualSortOrder is not in config', () => {
|
||||
const config = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
configurationType: WidgetConfigurationType.PIE_CHART,
|
||||
} as PieChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for wrong typename', () => {
|
||||
const config = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
configurationType: WidgetConfigurationType.BAR_CHART,
|
||||
manualSortOrder: ['a', 'b', 'c'],
|
||||
} as unknown as BarChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bar chart configuration', () => {
|
||||
it('should return primaryAxisManualSortOrder for primary axis', () => {
|
||||
const config = {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
primaryAxisManualSortOrder: ['x', 'y', 'z'],
|
||||
} as BarChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config, 'primary')).toEqual([
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return secondaryAxisManualSortOrder for secondary axis', () => {
|
||||
const config = {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
secondaryAxisManualSortOrder: ['1', '2', '3'],
|
||||
} as BarChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config, 'secondary')).toEqual([
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return undefined for null primaryAxisManualSortOrder', () => {
|
||||
const config = {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
primaryAxisManualSortOrder: null,
|
||||
} as BarChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config, 'primary')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('line chart configuration', () => {
|
||||
it('should return primaryAxisManualSortOrder for primary axis', () => {
|
||||
const config = {
|
||||
__typename: 'LineChartConfiguration' as const,
|
||||
primaryAxisManualSortOrder: ['a', 'b'],
|
||||
} as LineChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config, 'primary')).toEqual([
|
||||
'a',
|
||||
'b',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return secondaryAxisManualSortOrder for secondary axis', () => {
|
||||
const config = {
|
||||
__typename: 'LineChartConfiguration' as const,
|
||||
secondaryAxisManualSortOrder: ['c', 'd'],
|
||||
} as LineChartConfiguration;
|
||||
|
||||
expect(getManualSortOrderFromConfig(config, 'secondary')).toEqual([
|
||||
'c',
|
||||
'd',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
IconSortAscending,
|
||||
IconSortAscendingLetters,
|
||||
IconSortAscendingNumbers,
|
||||
IconSortDescending,
|
||||
IconSortDescendingLetters,
|
||||
IconSortDescendingNumbers,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
import { getSortIconForFieldType } from '@/command-menu/pages/page-layout/utils/getSortIconForFieldType';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
describe('getSortIconForFieldType', () => {
|
||||
describe('position sort', () => {
|
||||
it('should return IconSortAscending for FIELD_POSITION_ASC', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
orderBy: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscending);
|
||||
});
|
||||
|
||||
it('should return IconSortDescending for FIELD_POSITION_DESC', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
orderBy: GraphOrderBy.FIELD_POSITION_DESC,
|
||||
}),
|
||||
).toBe(IconSortDescending);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text field types', () => {
|
||||
it('should return IconSortAscendingLetters for TEXT field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingLetters);
|
||||
});
|
||||
|
||||
it('should return IconSortDescendingLetters for TEXT field descending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
}),
|
||||
).toBe(IconSortDescendingLetters);
|
||||
});
|
||||
|
||||
it('should return IconSortAscendingLetters for RICH_TEXT field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.RICH_TEXT,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingLetters);
|
||||
});
|
||||
|
||||
it('should return IconSortAscendingLetters for RICH_TEXT_V2 field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.RICH_TEXT_V2,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingLetters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('number field types', () => {
|
||||
it('should return IconSortAscendingNumbers for NUMBER field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingNumbers);
|
||||
});
|
||||
|
||||
it('should return IconSortDescendingNumbers for NUMBER field descending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
}),
|
||||
).toBe(IconSortDescendingNumbers);
|
||||
});
|
||||
|
||||
it('should return IconSortAscendingNumbers for CURRENCY field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.CURRENCY,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingNumbers);
|
||||
});
|
||||
|
||||
it('should return IconSortAscendingNumbers for RATING field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.RATING,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingNumbers);
|
||||
});
|
||||
});
|
||||
|
||||
describe('select field type', () => {
|
||||
it('should return IconSortAscendingLetters for SELECT field with FIELD_ASC', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingLetters);
|
||||
});
|
||||
|
||||
it('should return IconSortDescendingLetters for SELECT field with FIELD_DESC', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
}),
|
||||
).toBe(IconSortDescendingLetters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('undefined field type', () => {
|
||||
it('should return IconSortAscending for undefined field type ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: undefined,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscending);
|
||||
});
|
||||
|
||||
it('should return IconSortDescending for undefined field type descending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: undefined,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
}),
|
||||
).toBe(IconSortDescending);
|
||||
});
|
||||
});
|
||||
|
||||
describe('value-based sort', () => {
|
||||
it('should return IconSortAscending for VALUE_ASC', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
orderBy: GraphOrderBy.VALUE_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscendingNumbers);
|
||||
});
|
||||
|
||||
it('should return IconSortDescending for VALUE_DESC', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
}),
|
||||
).toBe(IconSortDescendingNumbers);
|
||||
});
|
||||
|
||||
it('should return IconSortAscending for VALUE_ASC with undefined field type', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: undefined,
|
||||
orderBy: GraphOrderBy.VALUE_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscending);
|
||||
});
|
||||
|
||||
it('should return IconSortDescending for VALUE_DESC with undefined field type', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: undefined,
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
}),
|
||||
).toBe(IconSortDescending);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default cases', () => {
|
||||
it('should return IconSortAscending for DATE field ascending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.DATE,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe(IconSortAscending);
|
||||
});
|
||||
|
||||
it('should return IconSortDescending for DATE field descending', () => {
|
||||
expect(
|
||||
getSortIconForFieldType({
|
||||
fieldType: FieldMetadataType.DATE,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
}),
|
||||
).toBe(IconSortDescending);
|
||||
});
|
||||
});
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { getSortLabelSuffixForFieldType } from '@/command-menu/pages/page-layout/utils/getSortLabelSuffixForFieldType';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
describe('getSortLabelSuffixForFieldType', () => {
|
||||
it('returns alphabetical for TEXT field with FIELD_ASC', () => {
|
||||
expect(
|
||||
getSortLabelSuffixForFieldType({
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe('alphabetical');
|
||||
});
|
||||
|
||||
it('returns ascending for NUMBER field with FIELD_ASC', () => {
|
||||
expect(
|
||||
getSortLabelSuffixForFieldType({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
}),
|
||||
).toBe('ascending');
|
||||
});
|
||||
|
||||
it('returns position ascending for SELECT field with FIELD_POSITION_ASC', () => {
|
||||
expect(
|
||||
getSortLabelSuffixForFieldType({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
orderBy: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
}),
|
||||
).toBe('position ascending');
|
||||
});
|
||||
});
|
||||
+13
-40
@@ -1,4 +1,5 @@
|
||||
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
|
||||
import { getChartDefaultOrderByForFieldType } from '@/command-menu/pages/page-layout/utils/getChartDefaultOrderByForFieldType';
|
||||
import { isFieldOrRelationNestedFieldDateKind } from '@/command-menu/pages/page-layout/utils/isFieldOrNestedFieldDateKind';
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
@@ -51,10 +52,15 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
'PieChartConfiguration',
|
||||
);
|
||||
|
||||
if (isPrimaryAxis) {
|
||||
const existingOrderBy =
|
||||
isBarChart || isLineChart ? configuration.primaryAxisOrderBy : null;
|
||||
const fieldMetadataItem = objectMetadataItem?.fields?.find(
|
||||
(field) => field.id === fieldId,
|
||||
);
|
||||
|
||||
const defaultOrderBy = isDefined(fieldMetadataItem?.type)
|
||||
? getChartDefaultOrderByForFieldType(fieldMetadataItem?.type)
|
||||
: GraphOrderBy.FIELD_ASC;
|
||||
|
||||
if (isPrimaryAxis) {
|
||||
const existingDateGranularity =
|
||||
isBarChart || isLineChart
|
||||
? configuration.primaryAxisDateGranularity
|
||||
@@ -73,19 +79,9 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
!isNewFieldDateType &&
|
||||
(isBarChart || isLineChart);
|
||||
|
||||
const isCurrentOrderByValueBased =
|
||||
existingOrderBy === GraphOrderBy.VALUE_ASC ||
|
||||
existingOrderBy === GraphOrderBy.VALUE_DESC;
|
||||
|
||||
const shouldResetOrderBy = isNewFieldDateType && isCurrentOrderByValueBased;
|
||||
|
||||
const newOrderBy = shouldResetOrderBy
|
||||
? GraphOrderBy.FIELD_ASC
|
||||
: (existingOrderBy ?? GraphOrderBy.FIELD_ASC);
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
primaryAxisOrderBy: isDefined(fieldId) ? newOrderBy : null,
|
||||
primaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
|
||||
primaryAxisDateGranularity: isDefined(fieldId)
|
||||
? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY)
|
||||
: null,
|
||||
@@ -94,32 +90,13 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
}
|
||||
|
||||
if (isPieChartGroupBy) {
|
||||
const existingOrderBy = isPieChart ? configuration.orderBy : null;
|
||||
|
||||
const existingDateGranularity = isPieChart
|
||||
? configuration.dateGranularity
|
||||
: null;
|
||||
|
||||
const isNewFieldDateType = isFieldOrRelationNestedFieldDateKind({
|
||||
fieldId,
|
||||
subFieldName,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
const isCurrentOrderByValueBased =
|
||||
existingOrderBy === GraphOrderBy.VALUE_ASC ||
|
||||
existingOrderBy === GraphOrderBy.VALUE_DESC;
|
||||
|
||||
const shouldResetOrderBy = isNewFieldDateType && isCurrentOrderByValueBased;
|
||||
|
||||
const newOrderBy = shouldResetOrderBy
|
||||
? GraphOrderBy.FIELD_ASC
|
||||
: (existingOrderBy ?? GraphOrderBy.FIELD_ASC);
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
orderBy: isDefined(fieldId) ? newOrderBy : null,
|
||||
orderBy: isDefined(fieldId) ? defaultOrderBy : null,
|
||||
dateGranularity: isDefined(fieldId)
|
||||
? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY)
|
||||
: null,
|
||||
@@ -133,9 +110,7 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
if (isBarChart) {
|
||||
return {
|
||||
...baseConfig,
|
||||
secondaryAxisOrderBy: isDefined(fieldId)
|
||||
? (configuration.secondaryAxisOrderBy ?? GraphOrderBy.FIELD_ASC)
|
||||
: null,
|
||||
secondaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
|
||||
secondaryAxisGroupByDateGranularity: isDefined(fieldId)
|
||||
? (configuration.secondaryAxisGroupByDateGranularity ??
|
||||
ObjectRecordGroupByDateGranularity.DAY)
|
||||
@@ -149,9 +124,7 @@ export const buildChartGroupByFieldConfigUpdate = <
|
||||
if (isLineChart) {
|
||||
return {
|
||||
...baseConfig,
|
||||
secondaryAxisOrderBy: isDefined(fieldId)
|
||||
? (configuration.secondaryAxisOrderBy ?? GraphOrderBy.FIELD_ASC)
|
||||
: null,
|
||||
secondaryAxisOrderBy: isDefined(fieldId) ? defaultOrderBy : null,
|
||||
secondaryAxisGroupByDateGranularity: isDefined(fieldId)
|
||||
? (configuration.secondaryAxisGroupByDateGranularity ??
|
||||
ObjectRecordGroupByDateGranularity.DAY)
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
isFieldMetadataDateKind,
|
||||
isFieldMetadataSelectKind,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export type SortOption = {
|
||||
value: GraphOrderBy;
|
||||
icon?: IconComponent | null;
|
||||
};
|
||||
|
||||
type FilterSortOptionsParams = {
|
||||
options: SortOption[];
|
||||
fieldType: FieldMetadataType;
|
||||
};
|
||||
|
||||
export const filterSortOptionsByFieldType = ({
|
||||
options,
|
||||
fieldType,
|
||||
}: FilterSortOptionsParams): SortOption[] => {
|
||||
return options.filter((option) => {
|
||||
const isValueSort =
|
||||
option.value === GraphOrderBy.VALUE_ASC ||
|
||||
option.value === GraphOrderBy.VALUE_DESC;
|
||||
|
||||
const isManualSort = option.value === GraphOrderBy.MANUAL;
|
||||
|
||||
const isPositionSort =
|
||||
option.value === GraphOrderBy.FIELD_POSITION_ASC ||
|
||||
option.value === GraphOrderBy.FIELD_POSITION_DESC;
|
||||
|
||||
const isSelectField = isFieldMetadataSelectKind(fieldType);
|
||||
|
||||
if (isManualSort && !isSelectField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPositionSort && !isSelectField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFieldMetadataDateKind(fieldType)) {
|
||||
return !isValueSort;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const getChartDefaultOrderByForFieldType = (
|
||||
fieldType: FieldMetadataType,
|
||||
): GraphOrderBy => {
|
||||
const isSelectField = fieldType === FieldMetadataType.SELECT;
|
||||
|
||||
return isSelectField
|
||||
? GraphOrderBy.FIELD_POSITION_ASC
|
||||
: GraphOrderBy.FIELD_ASC;
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
type FieldOption = {
|
||||
value: string;
|
||||
position?: number | null;
|
||||
};
|
||||
|
||||
export const getDefaultManualSortOrder = (
|
||||
options: FieldOption[] | null | undefined,
|
||||
): string[] => {
|
||||
if (!options) return [];
|
||||
|
||||
const sortedByPosition = options.toSorted(
|
||||
(a, b) => (a.position ?? 0) - (b.position ?? 0),
|
||||
);
|
||||
|
||||
return sortedByPosition.map((option) => option.value);
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type WidgetConfiguration } from '~/generated/graphql';
|
||||
|
||||
export const getManualSortOrderFromConfig = (
|
||||
configuration: WidgetConfiguration,
|
||||
axis?: 'primary' | 'secondary',
|
||||
): string[] | undefined => {
|
||||
if (isWidgetConfigurationOfType(configuration, 'PieChartConfiguration')) {
|
||||
return configuration.manualSortOrder ?? undefined;
|
||||
}
|
||||
|
||||
if (!isDefined(axis)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
isWidgetConfigurationOfType(configuration, 'BarChartConfiguration') ||
|
||||
isWidgetConfigurationOfType(configuration, 'LineChartConfiguration')
|
||||
) {
|
||||
if (axis === 'primary') {
|
||||
return configuration.primaryAxisManualSortOrder ?? undefined;
|
||||
}
|
||||
|
||||
return configuration.secondaryAxisManualSortOrder ?? undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
isFieldMetadataNumericKind,
|
||||
isFieldMetadataTextKind,
|
||||
} from 'twenty-shared/utils';
|
||||
import {
|
||||
type IconComponent,
|
||||
IconSortAscending,
|
||||
IconSortAscendingLetters,
|
||||
IconSortAscendingNumbers,
|
||||
IconSortDescending,
|
||||
IconSortDescendingLetters,
|
||||
IconSortDescendingNumbers,
|
||||
} from 'twenty-ui/display';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const getSortIconForFieldType = ({
|
||||
fieldType,
|
||||
orderBy,
|
||||
}: {
|
||||
fieldType: FieldMetadataType | undefined;
|
||||
orderBy: GraphOrderBy;
|
||||
}): IconComponent => {
|
||||
const isAscending =
|
||||
orderBy === GraphOrderBy.FIELD_ASC ||
|
||||
orderBy === GraphOrderBy.FIELD_POSITION_ASC ||
|
||||
orderBy === GraphOrderBy.VALUE_ASC;
|
||||
|
||||
const isPositionSort =
|
||||
orderBy === GraphOrderBy.FIELD_POSITION_ASC ||
|
||||
orderBy === GraphOrderBy.FIELD_POSITION_DESC;
|
||||
|
||||
if (isPositionSort) {
|
||||
return isAscending ? IconSortAscending : IconSortDescending;
|
||||
}
|
||||
|
||||
if (!fieldType) {
|
||||
return isAscending ? IconSortAscending : IconSortDescending;
|
||||
}
|
||||
|
||||
if (isFieldMetadataTextKind(fieldType)) {
|
||||
return isAscending ? IconSortAscendingLetters : IconSortDescendingLetters;
|
||||
}
|
||||
|
||||
if (isFieldMetadataNumericKind(fieldType)) {
|
||||
return isAscending ? IconSortAscendingNumbers : IconSortDescendingNumbers;
|
||||
}
|
||||
|
||||
if (
|
||||
fieldType === FieldMetadataType.SELECT &&
|
||||
(orderBy === GraphOrderBy.FIELD_ASC || orderBy === GraphOrderBy.FIELD_DESC)
|
||||
) {
|
||||
return isAscending ? IconSortAscendingLetters : IconSortDescendingLetters;
|
||||
}
|
||||
|
||||
return isAscending ? IconSortAscending : IconSortDescending;
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
isFieldMetadataNumericKind,
|
||||
isFieldMetadataTextKind,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const getSortLabelSuffixForFieldType = ({
|
||||
fieldType,
|
||||
orderBy,
|
||||
}: {
|
||||
fieldType: FieldMetadataType | undefined;
|
||||
orderBy: GraphOrderBy;
|
||||
}): string => {
|
||||
const isAscending =
|
||||
orderBy === GraphOrderBy.FIELD_ASC ||
|
||||
orderBy === GraphOrderBy.FIELD_POSITION_ASC ||
|
||||
orderBy === GraphOrderBy.VALUE_ASC;
|
||||
|
||||
if (!isDefined(fieldType)) {
|
||||
return isAscending ? t`ascending` : t`descending`;
|
||||
}
|
||||
|
||||
if (isFieldMetadataTextKind(fieldType)) {
|
||||
return isAscending ? t`alphabetical` : t`reverse alphabetical`;
|
||||
}
|
||||
|
||||
if (isFieldMetadataNumericKind(fieldType)) {
|
||||
return isAscending ? t`ascending` : t`descending`;
|
||||
}
|
||||
|
||||
if (fieldType === FieldMetadataType.SELECT) {
|
||||
if (
|
||||
orderBy === GraphOrderBy.FIELD_ASC ||
|
||||
orderBy === GraphOrderBy.FIELD_DESC
|
||||
) {
|
||||
return isAscending ? t`alphabetical` : t`reverse alphabetical`;
|
||||
}
|
||||
|
||||
if (
|
||||
orderBy === GraphOrderBy.FIELD_POSITION_ASC ||
|
||||
orderBy === GraphOrderBy.FIELD_POSITION_DESC
|
||||
) {
|
||||
return isAscending ? t`position ascending` : t`position descending`;
|
||||
}
|
||||
|
||||
return isAscending ? t`ascending` : t`descending`;
|
||||
}
|
||||
|
||||
return isAscending ? t`ascending` : t`descending`;
|
||||
};
|
||||
+5
@@ -24,10 +24,12 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
primaryAxisGroupBySubFieldName
|
||||
primaryAxisDateGranularity
|
||||
primaryAxisOrderBy
|
||||
primaryAxisManualSortOrder
|
||||
secondaryAxisGroupByFieldMetadataId
|
||||
secondaryAxisGroupBySubFieldName
|
||||
secondaryAxisGroupByDateGranularity
|
||||
secondaryAxisOrderBy
|
||||
secondaryAxisManualSortOrder
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
@@ -51,10 +53,12 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
primaryAxisGroupBySubFieldName
|
||||
primaryAxisDateGranularity
|
||||
primaryAxisOrderBy
|
||||
primaryAxisManualSortOrder
|
||||
secondaryAxisGroupByFieldMetadataId
|
||||
secondaryAxisGroupBySubFieldName
|
||||
secondaryAxisGroupByDateGranularity
|
||||
secondaryAxisOrderBy
|
||||
secondaryAxisManualSortOrder
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
@@ -77,6 +81,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
groupBySubFieldName
|
||||
dateGranularity
|
||||
orderBy
|
||||
manualSortOrder
|
||||
displayDataLabel
|
||||
showCenterMetric
|
||||
displayLegend
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { applyCumulativeTransformToBarChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToBarChartData';
|
||||
import { applyCumulativeTransformToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/applyCumulativeTransformToBarChartData';
|
||||
|
||||
describe('applyCumulativeTransformToBarChartData', () => {
|
||||
const testCases = [
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { applyCumulativeTransformToTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToTwoDimensionalBarChartData';
|
||||
import { applyCumulativeTransformToTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/applyCumulativeTransformToTwoDimensionalBarChartData';
|
||||
|
||||
describe('applyCumulativeTransformToTwoDimensionalBarChartData', () => {
|
||||
const testCases = [
|
||||
+2
-2
@@ -5,8 +5,8 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
type ApplyCumulativeTransformToTwoDimensionalBarChartDataOptions = {
|
||||
data: BarDatum[];
|
||||
keys: string[];
|
||||
rangeMin?: number;
|
||||
rangeMax?: number;
|
||||
rangeMin?: number | null;
|
||||
rangeMax?: number | null;
|
||||
};
|
||||
|
||||
export const applyCumulativeTransformToTwoDimensionalBarChartData = ({
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { type ProcessedTwoDimensionalDataPoint } from '@/page-layout/widgets/graph/utils/processTwoDimensionalGroupByResults';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
|
||||
type BuildTwoDimensionalBarChartDataParams = {
|
||||
processedDataPoints: ProcessedTwoDimensionalDataPoint[];
|
||||
indexByKey: string;
|
||||
};
|
||||
|
||||
type BuildTwoDimensionalBarChartDataResult = {
|
||||
unsortedData: BarDatum[];
|
||||
yValues: Set<string>;
|
||||
};
|
||||
|
||||
export const buildTwoDimensionalBarChartData = ({
|
||||
processedDataPoints,
|
||||
indexByKey,
|
||||
}: BuildTwoDimensionalBarChartDataParams): BuildTwoDimensionalBarChartDataResult => {
|
||||
const dataMap = new Map<string, BarDatum>();
|
||||
const yValues = new Set<string>();
|
||||
|
||||
for (const { xValue, yValue, aggregateValue } of processedDataPoints) {
|
||||
yValues.add(yValue);
|
||||
|
||||
if (!dataMap.has(xValue)) {
|
||||
dataMap.set(xValue, {
|
||||
[indexByKey]: xValue,
|
||||
});
|
||||
}
|
||||
|
||||
const dataItem = dataMap.get(xValue)!;
|
||||
dataItem[yValue] = aggregateValue;
|
||||
}
|
||||
|
||||
return {
|
||||
unsortedData: Array.from(dataMap.values()),
|
||||
yValues,
|
||||
};
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { BarChartGroupMode } from '~/generated/graphql';
|
||||
|
||||
type LimitTwoDimensionalBarChartDataParams = {
|
||||
sortedData: BarDatum[];
|
||||
sortedKeys: string[];
|
||||
sortedSeries: BarChartSeries[];
|
||||
groupMode?: BarChartGroupMode | null;
|
||||
};
|
||||
|
||||
type LimitTwoDimensionalBarChartDataResult = {
|
||||
limitedData: BarDatum[];
|
||||
limitedKeys: string[];
|
||||
limitedSeries: BarChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
};
|
||||
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
export const limitTwoDimensionalBarChartData = ({
|
||||
sortedData,
|
||||
sortedKeys,
|
||||
sortedSeries,
|
||||
groupMode,
|
||||
}: LimitTwoDimensionalBarChartDataParams): LimitTwoDimensionalBarChartDataResult => {
|
||||
const effectiveGroupMode = groupMode ?? BarChartGroupMode.STACKED;
|
||||
|
||||
const hasTooManyBars =
|
||||
sortedData.length > BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS;
|
||||
const hasTooManyGroups =
|
||||
sortedKeys.length > BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_GROUPS_PER_BAR;
|
||||
|
||||
const limitedData = sortedData.slice(
|
||||
0,
|
||||
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS,
|
||||
);
|
||||
const limitedKeys = sortedKeys.slice(
|
||||
0,
|
||||
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_GROUPS_PER_BAR,
|
||||
);
|
||||
const limitedSeries = sortedSeries.slice(
|
||||
0,
|
||||
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_GROUPS_PER_BAR,
|
||||
);
|
||||
|
||||
if (effectiveGroupMode === BarChartGroupMode.STACKED) {
|
||||
return {
|
||||
limitedData,
|
||||
limitedKeys,
|
||||
limitedSeries,
|
||||
hasTooManyGroups: hasTooManyBars || hasTooManyGroups,
|
||||
};
|
||||
}
|
||||
|
||||
const totalSegments = limitedData.length * limitedKeys.length;
|
||||
const hasTooManySegments =
|
||||
totalSegments > BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS;
|
||||
|
||||
if (!hasTooManySegments) {
|
||||
return {
|
||||
limitedData,
|
||||
limitedKeys,
|
||||
limitedSeries,
|
||||
hasTooManyGroups: hasTooManyBars || hasTooManyGroups,
|
||||
};
|
||||
}
|
||||
|
||||
const maxXValues = Math.floor(
|
||||
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS / limitedKeys.length,
|
||||
);
|
||||
const furtherLimitedData = limitedData.slice(0, Math.max(1, maxXValues));
|
||||
|
||||
return {
|
||||
limitedData: furtherLimitedData,
|
||||
limitedKeys,
|
||||
limitedSeries,
|
||||
hasTooManyGroups: true,
|
||||
};
|
||||
};
|
||||
+2
@@ -23,9 +23,11 @@ export const sortBarChartDataBySecondaryDimensionSum = ({
|
||||
const dataWithSecondaryDimensionSums = data.map((datum) => {
|
||||
const secondaryDimensionSum = keys.reduce((sumAccumulator, segmentKey) => {
|
||||
const segmentValue = datum[segmentKey];
|
||||
|
||||
if (isDefined(segmentValue) && typeof segmentValue === 'number') {
|
||||
return sumAccumulator + segmentValue;
|
||||
}
|
||||
|
||||
return sumAccumulator;
|
||||
}, 0);
|
||||
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortBarChartDataBySecondaryDimensionSum';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { sortSecondaryAxisData } from '@/page-layout/widgets/graph/utils/sortSecondaryAxisData';
|
||||
import { sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually } from '@/page-layout/widgets/graph/utils/sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration, GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type SortTwoDimensionalBarChartDataConfiguration = {
|
||||
data: BarDatum[];
|
||||
keys: string[];
|
||||
indexByKey: string;
|
||||
configuration: BarChartConfiguration;
|
||||
primaryAxisFormattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
primaryAxisSelectFieldOptions?: FieldMetadataItemOption[] | null;
|
||||
secondaryAxisFormattedToRawLookup?: Map<string, RawDimensionValue>;
|
||||
secondaryAxisSelectFieldOptions?: FieldMetadataItemOption[] | null;
|
||||
};
|
||||
|
||||
type SortTwoDimensionalBarChartDataResult = {
|
||||
sortedData: BarDatum[];
|
||||
sortedKeys: string[];
|
||||
sortedSeries: BarChartSeries[];
|
||||
};
|
||||
|
||||
export const sortTwoDimensionalBarChartData = ({
|
||||
data,
|
||||
keys,
|
||||
indexByKey,
|
||||
configuration: {
|
||||
primaryAxisOrderBy,
|
||||
primaryAxisManualSortOrder,
|
||||
secondaryAxisOrderBy,
|
||||
secondaryAxisManualSortOrder,
|
||||
color,
|
||||
},
|
||||
primaryAxisFormattedToRawLookup,
|
||||
primaryAxisSelectFieldOptions,
|
||||
secondaryAxisFormattedToRawLookup,
|
||||
secondaryAxisSelectFieldOptions,
|
||||
}: SortTwoDimensionalBarChartDataConfiguration): SortTwoDimensionalBarChartDataResult => {
|
||||
const sortedKeys = sortSecondaryAxisData({
|
||||
items: keys,
|
||||
orderBy: secondaryAxisOrderBy,
|
||||
manualSortOrder: secondaryAxisManualSortOrder,
|
||||
formattedToRawLookup: secondaryAxisFormattedToRawLookup,
|
||||
selectFieldOptions: secondaryAxisSelectFieldOptions,
|
||||
getFormattedValue: (item) => item,
|
||||
});
|
||||
|
||||
const sortedSeries: BarChartSeries[] = sortedKeys.map((key) => ({
|
||||
key,
|
||||
label: key,
|
||||
color: color as GraphColor,
|
||||
}));
|
||||
|
||||
let sortedData: BarDatum[] = data;
|
||||
|
||||
if (isDefined(primaryAxisOrderBy)) {
|
||||
if (
|
||||
primaryAxisOrderBy === GraphOrderBy.VALUE_ASC ||
|
||||
primaryAxisOrderBy === GraphOrderBy.VALUE_DESC
|
||||
) {
|
||||
sortedData = sortBarChartDataBySecondaryDimensionSum({
|
||||
data,
|
||||
keys: sortedKeys,
|
||||
orderBy: primaryAxisOrderBy,
|
||||
});
|
||||
} else {
|
||||
sortedData = sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually({
|
||||
data,
|
||||
orderBy: primaryAxisOrderBy,
|
||||
manualSortOrder: primaryAxisManualSortOrder,
|
||||
formattedToRawLookup: primaryAxisFormattedToRawLookup,
|
||||
getFormattedValue: (datum) => datum[indexByKey] as string,
|
||||
selectFieldOptions: primaryAxisSelectFieldOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sortedData,
|
||||
sortedKeys,
|
||||
sortedSeries,
|
||||
};
|
||||
};
|
||||
+42
-54
@@ -1,20 +1,20 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant';
|
||||
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { applyCumulativeTransformToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/applyCumulativeTransformToBarChartData';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { applyCumulativeTransformToBarChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToBarChartData';
|
||||
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
import { processOneDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processOneDimensionalGroupByResults';
|
||||
import { sortChartData } from '@/page-layout/widgets/graph/utils/sortChartData';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { type FirstDayOfTheWeek, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FirstDayOfTheWeek,
|
||||
isFieldMetadataSelectKind,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformOneDimensionalGroupByToBarChartDataParams = {
|
||||
@@ -59,55 +59,43 @@ export const transformOneDimensionalGroupByToBarChartData = ({
|
||||
? `${aggregateField.name}-aggregate`
|
||||
: aggregateField.name;
|
||||
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
const limitedResults = rawResults.slice(
|
||||
const { processedDataPoints, formattedToRawLookup } =
|
||||
processOneDimensionalGroupByResults({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
const unsortedData: BarDatum[] = processedDataPoints.map(
|
||||
({ xValue, aggregateValue }) => ({
|
||||
[indexByKey]: xValue,
|
||||
[aggregateValueKey]: aggregateValue,
|
||||
}),
|
||||
);
|
||||
|
||||
const sortedData = sortChartData({
|
||||
data: unsortedData,
|
||||
orderBy: configuration.primaryAxisOrderBy,
|
||||
manualSortOrder: configuration.primaryAxisManualSortOrder,
|
||||
formattedToRawLookup,
|
||||
getFieldValue: (datum) => datum[indexByKey] as string,
|
||||
getNumericValue: (datum) => datum[aggregateValueKey] as number,
|
||||
selectFieldOptions: isFieldMetadataSelectKind(groupByFieldX.type)
|
||||
? groupByFieldX.options
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const limitedSortedData = sortedData.slice(
|
||||
0,
|
||||
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS,
|
||||
);
|
||||
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: limitedResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
primaryAxisDateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
|
||||
const data: BarDatum[] = limitedResults.map((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
|
||||
const xValue = isDefined(dimensionValues?.[0])
|
||||
? formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByFieldX,
|
||||
dateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
subFieldName:
|
||||
configuration.primaryAxisGroupBySubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
})
|
||||
: '';
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
[indexByKey]: xValue,
|
||||
[aggregateValueKey]: aggregateValue,
|
||||
};
|
||||
});
|
||||
|
||||
const series: BarChartSeries[] = [
|
||||
{
|
||||
key: aggregateValueKey,
|
||||
@@ -118,12 +106,12 @@ export const transformOneDimensionalGroupByToBarChartData = ({
|
||||
|
||||
const finalData = configuration.isCumulative
|
||||
? applyCumulativeTransformToBarChartData({
|
||||
data,
|
||||
data: limitedSortedData,
|
||||
aggregateKey: aggregateValueKey,
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
})
|
||||
: data;
|
||||
: limitedSortedData;
|
||||
|
||||
return {
|
||||
data: finalData,
|
||||
|
||||
+45
-130
@@ -1,25 +1,17 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortBarChartDataBySecondaryDimensionSum';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { applyCumulativeTransformToTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/applyCumulativeTransformToTwoDimensionalBarChartData';
|
||||
import { buildTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/buildTwoDimensionalBarChartData';
|
||||
import { limitTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/limitTwoDimensionalBarChartData';
|
||||
import { sortTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortTwoDimensionalBarChartData';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { applyCumulativeTransformToTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToTwoDimensionalBarChartData';
|
||||
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
import { getSortedKeys } from '@/page-layout/widgets/graph/utils/getSortedKeys';
|
||||
import { processTwoDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processTwoDimensionalGroupByResults';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { isDefined, type FirstDayOfTheWeek } from 'twenty-shared/utils';
|
||||
import {
|
||||
BarChartGroupMode,
|
||||
type BarChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
import { type FirstDayOfTheWeek } from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformTwoDimensionalGroupByToBarChartDataParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
@@ -60,136 +52,59 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
|
||||
subFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
});
|
||||
|
||||
const dataMap = new Map<string, BarDatum>();
|
||||
const xValues = new Set<string>();
|
||||
const yValues = new Set<string>();
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: rawResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
primaryAxisDateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
|
||||
let hasTooManyGroups = false;
|
||||
|
||||
rawResults.forEach((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
if (!isDefined(dimensionValues) || dimensionValues.length < 2) return;
|
||||
|
||||
const xValue = formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByFieldX,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity ?? undefined,
|
||||
subFieldName: configuration.primaryAxisGroupBySubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
const yValue = formatDimensionValue({
|
||||
value: dimensionValues[1],
|
||||
fieldMetadata: groupByFieldY,
|
||||
dateGranularity:
|
||||
configuration.secondaryAxisGroupByDateGranularity ?? undefined,
|
||||
subFieldName: configuration.secondaryAxisGroupBySubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
if (isDefined(dimensionValues[0])) {
|
||||
formattedToRawLookup.set(xValue, dimensionValues[0] as RawDimensionValue);
|
||||
}
|
||||
|
||||
// TODO: Add a limit to the query instead of checking here (issue: twentyhq/core-team-issues#1600)
|
||||
const isNewX = !xValues.has(xValue);
|
||||
const isNewY = !yValues.has(yValue);
|
||||
|
||||
if (configuration.groupMode === BarChartGroupMode.STACKED) {
|
||||
if (
|
||||
isNewX &&
|
||||
xValues.size >= BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS
|
||||
) {
|
||||
hasTooManyGroups = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (configuration.groupMode === BarChartGroupMode.GROUPED) {
|
||||
const totalUniqueDimensions = xValues.size * yValues.size;
|
||||
const additionalDimensions =
|
||||
(isNewX ? 1 : 0) * yValues.size + (isNewY ? 1 : 0) * xValues.size;
|
||||
|
||||
if (
|
||||
totalUniqueDimensions + additionalDimensions >
|
||||
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS
|
||||
) {
|
||||
hasTooManyGroups = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
const { processedDataPoints, formattedToRawLookup, yFormattedToRawLookup } =
|
||||
processTwoDimensionalGroupByResults({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
if (!isDefined(aggregateValue)) return;
|
||||
|
||||
xValues.add(xValue);
|
||||
yValues.add(yValue);
|
||||
|
||||
if (!dataMap.has(xValue)) {
|
||||
dataMap.set(xValue, {
|
||||
[indexByKey]: xValue,
|
||||
});
|
||||
}
|
||||
|
||||
const dataItem = dataMap.get(xValue)!;
|
||||
dataItem[yValue] = aggregateValue;
|
||||
const { unsortedData, yValues } = buildTwoDimensionalBarChartData({
|
||||
processedDataPoints,
|
||||
indexByKey,
|
||||
});
|
||||
|
||||
// Sorting needed because yValues may be unordered despite BE orderBy, if there are empty groups
|
||||
const keys = getSortedKeys({
|
||||
orderByY: configuration.secondaryAxisOrderBy,
|
||||
yValues: Array.from(yValues),
|
||||
});
|
||||
const { sortedData, sortedKeys, sortedSeries } =
|
||||
sortTwoDimensionalBarChartData({
|
||||
data: unsortedData,
|
||||
keys: Array.from(yValues),
|
||||
indexByKey,
|
||||
configuration,
|
||||
primaryAxisFormattedToRawLookup: formattedToRawLookup,
|
||||
primaryAxisSelectFieldOptions: groupByFieldX.options,
|
||||
secondaryAxisFormattedToRawLookup: yFormattedToRawLookup,
|
||||
secondaryAxisSelectFieldOptions: groupByFieldY.options,
|
||||
});
|
||||
|
||||
const series: BarChartSeries[] = keys.map((key) => ({
|
||||
key,
|
||||
label: key,
|
||||
color: configuration.color as GraphColor,
|
||||
}));
|
||||
|
||||
const unsortedData = Array.from(dataMap.values());
|
||||
const sortedData = isDefined(configuration.primaryAxisOrderBy)
|
||||
? sortBarChartDataBySecondaryDimensionSum({
|
||||
data: unsortedData,
|
||||
keys,
|
||||
orderBy: configuration.primaryAxisOrderBy,
|
||||
})
|
||||
: unsortedData;
|
||||
const { limitedData, limitedKeys, limitedSeries, hasTooManyGroups } =
|
||||
limitTwoDimensionalBarChartData({
|
||||
sortedData,
|
||||
sortedKeys,
|
||||
sortedSeries,
|
||||
groupMode: configuration.groupMode,
|
||||
});
|
||||
|
||||
const finalData = configuration.isCumulative
|
||||
? applyCumulativeTransformToTwoDimensionalBarChartData({
|
||||
data: sortedData,
|
||||
keys,
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
data: limitedData,
|
||||
keys: limitedKeys,
|
||||
rangeMin: configuration.rangeMin,
|
||||
rangeMax: configuration.rangeMax,
|
||||
})
|
||||
: sortedData;
|
||||
: limitedData;
|
||||
|
||||
return {
|
||||
data: finalData,
|
||||
indexBy: indexByKey,
|
||||
keys,
|
||||
series,
|
||||
keys: limitedKeys,
|
||||
series: limitedSeries,
|
||||
hasTooManyGroups,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLin
|
||||
import { getLineChartQueryLimit } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartQueryLimit';
|
||||
import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { transformGroupByDataToLineChartData } from '@/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData';
|
||||
import { transformGroupByDataToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformGroupByDataToLineChartData';
|
||||
import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek';
|
||||
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData';
|
||||
import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/applyCumulativeTransformToLineChartData';
|
||||
|
||||
describe('applyCumulativeTransformToLineChartData', () => {
|
||||
const testCases = [
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { FirstDayOfTheWeek } from 'twenty-shared/types';
|
||||
import {
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
WidgetConfigurationType,
|
||||
type LineChartConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToLineChartData';
|
||||
|
||||
describe('transformOneDimensionalGroupByToLineChartData', () => {
|
||||
const userTimezone = 'Europe/Paris';
|
||||
@@ -97,7 +97,7 @@ describe('transformOneDimensionalGroupByToLineChartData', () => {
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter out null aggregate values', () => {
|
||||
it('should treat null aggregate values as zero', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['Stage A'],
|
||||
@@ -127,6 +127,7 @@ describe('transformOneDimensionalGroupByToLineChartData', () => {
|
||||
|
||||
expect(result.series[0].data).toEqual([
|
||||
{ x: 'Stage A', y: 100 },
|
||||
{ x: 'Stage B', y: 0 },
|
||||
{ x: 'Stage C', y: 200 },
|
||||
]);
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
+4
-4
@@ -1,5 +1,6 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { FirstDayOfTheWeek } from 'twenty-shared/types';
|
||||
import {
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
WidgetConfigurationType,
|
||||
type LineChartConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToLineChartData';
|
||||
|
||||
describe('transformTwoDimensionalGroupByToLineChartData', () => {
|
||||
const userTimezone = 'Europe/Paris';
|
||||
@@ -206,7 +206,7 @@ describe('transformTwoDimensionalGroupByToLineChartData', () => {
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter out null aggregate values', () => {
|
||||
it('should convert null aggregate values to zero', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01-01', 'Stage A'],
|
||||
@@ -235,8 +235,8 @@ describe('transformTwoDimensionalGroupByToLineChartData', () => {
|
||||
firstDayOfTheWeek: FirstDayOfTheWeek.MONDAY,
|
||||
});
|
||||
|
||||
expect(result.series[0].data).toHaveLength(2);
|
||||
expect(result.series[0].data.map((d) => d.y)).toEqual([100, 200]);
|
||||
expect(result.series[0].data).toHaveLength(3);
|
||||
expect(result.series[0].data.map((d) => d.y)).toEqual([100, 0, 200]);
|
||||
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type ProcessedTwoDimensionalDataPoint } from '@/page-layout/widgets/graph/utils/processTwoDimensionalGroupByResults';
|
||||
|
||||
type BuildTwoDimensionalLineChartSeriesParams = {
|
||||
processedDataPoints: ProcessedTwoDimensionalDataPoint[];
|
||||
color?: GraphColor | null;
|
||||
};
|
||||
|
||||
type BuildTwoDimensionalLineChartSeriesResult = {
|
||||
unsortedSeries: LineChartSeries[];
|
||||
};
|
||||
|
||||
export const buildTwoDimensionalLineChartSeries = ({
|
||||
processedDataPoints,
|
||||
color,
|
||||
}: BuildTwoDimensionalLineChartSeriesParams): BuildTwoDimensionalLineChartSeriesResult => {
|
||||
const seriesMap = new Map<string, Map<string, number>>();
|
||||
const allXValues: string[] = [];
|
||||
const xValueSet = new Set<string>();
|
||||
|
||||
for (const { xValue, yValue, aggregateValue } of processedDataPoints) {
|
||||
const isNewX = !xValueSet.has(xValue);
|
||||
|
||||
if (isNewX) {
|
||||
xValueSet.add(xValue);
|
||||
allXValues.push(xValue);
|
||||
}
|
||||
|
||||
if (!seriesMap.has(yValue)) {
|
||||
seriesMap.set(yValue, new Map());
|
||||
}
|
||||
|
||||
seriesMap.get(yValue)!.set(xValue, aggregateValue);
|
||||
}
|
||||
|
||||
const unsortedSeries: LineChartSeries[] = Array.from(seriesMap.entries()).map(
|
||||
([seriesKey, xToYMap]) => {
|
||||
const data: LineChartDataPoint[] = allXValues.map((xValue) => ({
|
||||
x: xValue,
|
||||
y: xToYMap.get(xValue) ?? 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
id: seriesKey,
|
||||
label: seriesKey,
|
||||
color: color ?? undefined,
|
||||
data,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
unsortedSeries,
|
||||
};
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
|
||||
type LimitTwoDimensionalLineChartDataParams = {
|
||||
sortedSeries: LineChartSeries[];
|
||||
isStacked: boolean;
|
||||
};
|
||||
|
||||
type LimitTwoDimensionalLineChartDataResult = {
|
||||
limitedSeries: LineChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
};
|
||||
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
export const limitTwoDimensionalLineChartData = ({
|
||||
sortedSeries,
|
||||
isStacked,
|
||||
}: LimitTwoDimensionalLineChartDataParams): LimitTwoDimensionalLineChartDataResult => {
|
||||
if (sortedSeries.length === 0) {
|
||||
return {
|
||||
limitedSeries: sortedSeries,
|
||||
hasTooManyGroups: false,
|
||||
};
|
||||
}
|
||||
|
||||
const maxSeries = isStacked
|
||||
? LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_STACKED_SERIES
|
||||
: LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_NON_STACKED_SERIES;
|
||||
|
||||
const hasTooManySeries = sortedSeries.length > maxSeries;
|
||||
|
||||
const dataPointCount = sortedSeries[0].data.length;
|
||||
const hasTooManyDataPoints =
|
||||
dataPointCount > LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS;
|
||||
|
||||
const hasTooManyGroups = hasTooManySeries || hasTooManyDataPoints;
|
||||
|
||||
if (!hasTooManyGroups) {
|
||||
return {
|
||||
limitedSeries: sortedSeries,
|
||||
hasTooManyGroups: false,
|
||||
};
|
||||
}
|
||||
|
||||
const seriesLimited = sortedSeries.slice(0, maxSeries);
|
||||
|
||||
const limitedSeries = seriesLimited.map((series) => ({
|
||||
...series,
|
||||
data: series.data.slice(
|
||||
0,
|
||||
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
),
|
||||
}));
|
||||
|
||||
return {
|
||||
limitedSeries,
|
||||
hasTooManyGroups: true,
|
||||
};
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type SortLineChartDataBySecondaryDimensionSumParams = {
|
||||
series: LineChartSeries[];
|
||||
orderBy: GraphOrderBy;
|
||||
};
|
||||
|
||||
export const sortLineChartDataBySecondaryDimensionSum = ({
|
||||
series,
|
||||
orderBy,
|
||||
}: SortLineChartDataBySecondaryDimensionSumParams): LineChartSeries[] => {
|
||||
if (series.length === 0) {
|
||||
return series;
|
||||
}
|
||||
|
||||
const allXValues = series[0].data.map((point) => point.x);
|
||||
|
||||
const seriesLookups = series.map(
|
||||
(seriesItem) => new Map(seriesItem.data.map((point) => [point.x, point.y])),
|
||||
);
|
||||
|
||||
const xValueSums = new Map<
|
||||
LineChartDataPoint['x'],
|
||||
LineChartDataPoint['y']
|
||||
>();
|
||||
|
||||
for (const xValue of allXValues) {
|
||||
const sum = seriesLookups.reduce((accumulator, lookup) => {
|
||||
return accumulator + (lookup.get(xValue) ?? 0);
|
||||
}, 0);
|
||||
xValueSums.set(xValue, sum);
|
||||
}
|
||||
|
||||
const sortedXValues = allXValues.toSorted((a, b) => {
|
||||
const sumA = xValueSums.get(a) ?? 0;
|
||||
const sumB = xValueSums.get(b) ?? 0;
|
||||
|
||||
return orderBy === GraphOrderBy.VALUE_ASC ? sumA - sumB : sumB - sumA;
|
||||
});
|
||||
|
||||
const xValueToIndex = new Map<LineChartDataPoint['x'], number>(
|
||||
sortedXValues.map((xValue, index) => [xValue, index]),
|
||||
);
|
||||
|
||||
return series.map((seriesItem) => ({
|
||||
...seriesItem,
|
||||
data: seriesItem.data.toSorted((a, b) => {
|
||||
const indexA = xValueToIndex.get(a.x) ?? 0;
|
||||
const indexB = xValueToIndex.get(b.x) ?? 0;
|
||||
|
||||
return indexA - indexB;
|
||||
}),
|
||||
}));
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { sortLineChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/sortLineChartDataBySecondaryDimensionSum';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { sortSecondaryAxisData } from '@/page-layout/widgets/graph/utils/sortSecondaryAxisData';
|
||||
import { sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually } from '@/page-layout/widgets/graph/utils/sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type LineChartConfiguration, GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type SortTwoDimensionalLineChartDataConfiguration = {
|
||||
series: LineChartSeries[];
|
||||
configuration: LineChartConfiguration;
|
||||
primaryAxisFormattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
primaryAxisSelectFieldOptions?: FieldMetadataItemOption[] | null;
|
||||
secondaryAxisFormattedToRawLookup?: Map<string, RawDimensionValue>;
|
||||
secondaryAxisSelectFieldOptions?: FieldMetadataItemOption[] | null;
|
||||
};
|
||||
|
||||
type SortTwoDimensionalLineChartDataResult = {
|
||||
sortedSeries: LineChartSeries[];
|
||||
};
|
||||
|
||||
export const sortTwoDimensionalLineChartData = ({
|
||||
series,
|
||||
configuration: {
|
||||
primaryAxisOrderBy,
|
||||
primaryAxisManualSortOrder,
|
||||
secondaryAxisOrderBy,
|
||||
secondaryAxisManualSortOrder,
|
||||
},
|
||||
primaryAxisFormattedToRawLookup,
|
||||
primaryAxisSelectFieldOptions,
|
||||
secondaryAxisFormattedToRawLookup,
|
||||
secondaryAxisSelectFieldOptions,
|
||||
}: SortTwoDimensionalLineChartDataConfiguration): SortTwoDimensionalLineChartDataResult => {
|
||||
let sortedSeries = series;
|
||||
|
||||
if (isDefined(primaryAxisOrderBy)) {
|
||||
if (
|
||||
primaryAxisOrderBy === GraphOrderBy.VALUE_ASC ||
|
||||
primaryAxisOrderBy === GraphOrderBy.VALUE_DESC
|
||||
) {
|
||||
sortedSeries = sortLineChartDataBySecondaryDimensionSum({
|
||||
series,
|
||||
orderBy: primaryAxisOrderBy,
|
||||
});
|
||||
} else {
|
||||
sortedSeries = series.map((seriesItem) => {
|
||||
const sortedDataPoints =
|
||||
sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually({
|
||||
data: seriesItem.data,
|
||||
orderBy: primaryAxisOrderBy,
|
||||
manualSortOrder: primaryAxisManualSortOrder,
|
||||
formattedToRawLookup: primaryAxisFormattedToRawLookup,
|
||||
getFormattedValue: (dataPoint: LineChartDataPoint) =>
|
||||
String(dataPoint.x),
|
||||
selectFieldOptions: primaryAxisSelectFieldOptions,
|
||||
});
|
||||
|
||||
return {
|
||||
...seriesItem,
|
||||
data: sortedDataPoints,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sortedSeries = sortSecondaryAxisData({
|
||||
items: sortedSeries,
|
||||
orderBy: secondaryAxisOrderBy,
|
||||
manualSortOrder: secondaryAxisManualSortOrder,
|
||||
formattedToRawLookup: secondaryAxisFormattedToRawLookup,
|
||||
selectFieldOptions: secondaryAxisSelectFieldOptions,
|
||||
getFormattedValue: (item) => item.id,
|
||||
});
|
||||
|
||||
return {
|
||||
sortedSeries,
|
||||
};
|
||||
};
|
||||
+2
-2
@@ -8,8 +8,8 @@ import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupBy
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults';
|
||||
import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind';
|
||||
import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToLineChartData';
|
||||
import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToLineChartData';
|
||||
import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData';
|
||||
import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData';
|
||||
import {
|
||||
type FirstDayOfTheWeek,
|
||||
isDefined,
|
||||
+42
-60
@@ -1,19 +1,19 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant';
|
||||
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/applyCumulativeTransformToLineChartData';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData';
|
||||
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { type FirstDayOfTheWeek, isDefined } from 'twenty-shared/utils';
|
||||
import { processOneDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processOneDimensionalGroupByResults';
|
||||
import { sortChartData } from '@/page-layout/widgets/graph/utils/sortChartData';
|
||||
import {
|
||||
isFieldMetadataSelectKind,
|
||||
type FirstDayOfTheWeek,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type LineChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformOneDimensionalGroupByToLineChartDataParams = {
|
||||
@@ -45,68 +45,50 @@ export const transformOneDimensionalGroupByToLineChartData = ({
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
}: TransformOneDimensionalGroupByToLineChartDataParams): TransformOneDimensionalGroupByToLineChartDataResult => {
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
const limitedResults = rawResults.slice(
|
||||
const { processedDataPoints, formattedToRawLookup } =
|
||||
processOneDimensionalGroupByResults({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
const unsortedData: LineChartDataPoint[] = processedDataPoints.map(
|
||||
({ xValue, aggregateValue }) => ({
|
||||
x: xValue,
|
||||
y: aggregateValue,
|
||||
}),
|
||||
);
|
||||
|
||||
const sortedData = sortChartData({
|
||||
data: unsortedData,
|
||||
orderBy: configuration.primaryAxisOrderBy,
|
||||
manualSortOrder: configuration.primaryAxisManualSortOrder,
|
||||
formattedToRawLookup,
|
||||
getFieldValue: (point) => String(point.x),
|
||||
getNumericValue: (point) => point.y ?? 0,
|
||||
selectFieldOptions: isFieldMetadataSelectKind(groupByFieldX.type)
|
||||
? groupByFieldX.options
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const limitedSortedData = sortedData.slice(
|
||||
0,
|
||||
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
);
|
||||
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: limitedResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
primaryAxisDateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
|
||||
const data: LineChartDataPoint[] = limitedResults
|
||||
.map((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
|
||||
const rawAggregateValue = result[aggregateOperation];
|
||||
if (!isDefined(rawAggregateValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const xValue = isDefined(dimensionValues?.[0])
|
||||
? formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByFieldX,
|
||||
dateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
subFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
})
|
||||
: '';
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
x: xValue,
|
||||
y: aggregateValue,
|
||||
};
|
||||
})
|
||||
.filter((point) => isDefined(point));
|
||||
|
||||
const transformedData = configuration.isCumulative
|
||||
? applyCumulativeTransformToLineChartData({
|
||||
data,
|
||||
data: limitedSortedData,
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
})
|
||||
: data;
|
||||
: limitedSortedData;
|
||||
|
||||
const series: LineChartSeries[] = [
|
||||
{
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/applyCumulativeTransformToLineChartData';
|
||||
import { buildTwoDimensionalLineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/buildTwoDimensionalLineChartSeries';
|
||||
import { limitTwoDimensionalLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/limitTwoDimensionalLineChartData';
|
||||
import { sortTwoDimensionalLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/sortTwoDimensionalLineChartData';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { processTwoDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processTwoDimensionalGroupByResults';
|
||||
import { type FirstDayOfTheWeek } from 'twenty-shared/utils';
|
||||
import { type LineChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformTwoDimensionalGroupByToLineChartDataParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
groupByFieldX: FieldMetadataItem;
|
||||
groupByFieldY: FieldMetadataItem;
|
||||
aggregateField: FieldMetadataItem;
|
||||
configuration: LineChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
primaryAxisSubFieldName?: string | null;
|
||||
userTimezone: string;
|
||||
firstDayOfTheWeek: FirstDayOfTheWeek;
|
||||
};
|
||||
|
||||
type TransformTwoDimensionalGroupByToLineChartDataResult = {
|
||||
series: LineChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
}: TransformTwoDimensionalGroupByToLineChartDataParams): TransformTwoDimensionalGroupByToLineChartDataResult => {
|
||||
const { processedDataPoints, formattedToRawLookup, yFormattedToRawLookup } =
|
||||
processTwoDimensionalGroupByResults({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
const { unsortedSeries } = buildTwoDimensionalLineChartSeries({
|
||||
processedDataPoints,
|
||||
color: configuration.color as GraphColor,
|
||||
});
|
||||
|
||||
const { sortedSeries } = sortTwoDimensionalLineChartData({
|
||||
series: unsortedSeries,
|
||||
configuration,
|
||||
primaryAxisFormattedToRawLookup: formattedToRawLookup,
|
||||
primaryAxisSelectFieldOptions: groupByFieldX.options,
|
||||
secondaryAxisFormattedToRawLookup: yFormattedToRawLookup,
|
||||
secondaryAxisSelectFieldOptions: groupByFieldY.options,
|
||||
});
|
||||
|
||||
const { limitedSeries, hasTooManyGroups } = limitTwoDimensionalLineChartData({
|
||||
sortedSeries,
|
||||
isStacked:
|
||||
configuration.isStacked ?? LINE_CHART_CONSTANTS.IS_STACKED_DEFAULT,
|
||||
});
|
||||
|
||||
const finalSeries = configuration.isCumulative
|
||||
? limitedSeries.map((seriesItem) => ({
|
||||
...seriesItem,
|
||||
data: applyCumulativeTransformToLineChartData({
|
||||
data: seriesItem.data,
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
}),
|
||||
}))
|
||||
: limitedSeries;
|
||||
|
||||
return {
|
||||
series: finalSeries,
|
||||
hasTooManyGroups,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
-4
@@ -1,5 +1,4 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups';
|
||||
import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant';
|
||||
@@ -36,7 +35,6 @@ export const useGraphPieChartWidgetData = ({
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const {
|
||||
data: groupByData,
|
||||
@@ -58,7 +56,6 @@ export const useGraphPieChartWidgetData = ({
|
||||
transformGroupByDataToPieChartData({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems: objectMetadataItems ?? [],
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
userTimezone,
|
||||
@@ -67,7 +64,6 @@ export const useGraphPieChartWidgetData = ({
|
||||
[
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
userTimezone,
|
||||
|
||||
-4
@@ -45,7 +45,6 @@ describe('transformGroupByDataToPieChartData', () => {
|
||||
namePlural: 'companies',
|
||||
fields: [groupByField, aggregateField],
|
||||
} as any;
|
||||
const objectMetadataItems = [objectMetadataItem];
|
||||
|
||||
const configuration = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
@@ -77,7 +76,6 @@ describe('transformGroupByDataToPieChartData', () => {
|
||||
const result = transformGroupByDataToPieChartData({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
configuration,
|
||||
aggregateOperation: 'COUNT',
|
||||
userTimezone,
|
||||
@@ -114,7 +112,6 @@ describe('transformGroupByDataToPieChartData', () => {
|
||||
namePlural: 'companies',
|
||||
fields: [groupByField, aggregateField],
|
||||
} as any;
|
||||
const objectMetadataItems = [objectMetadataItem];
|
||||
|
||||
const configuration = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
@@ -146,7 +143,6 @@ describe('transformGroupByDataToPieChartData', () => {
|
||||
const result = transformGroupByDataToPieChartData({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
configuration,
|
||||
aggregateOperation: 'COUNT',
|
||||
userTimezone,
|
||||
|
||||
+42
-52
@@ -1,6 +1,5 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
|
||||
import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant';
|
||||
import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant';
|
||||
@@ -8,21 +7,15 @@ import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPi
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind';
|
||||
import {
|
||||
type FirstDayOfTheWeek,
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils';
|
||||
import { processOneDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processOneDimensionalGroupByResults';
|
||||
import { sortChartData } from '@/page-layout/widgets/graph/utils/sortChartData';
|
||||
import { type FirstDayOfTheWeek } from 'twenty-shared/types';
|
||||
import { isDefined, isFieldMetadataSelectKind } from 'twenty-shared/utils';
|
||||
import { type PieChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformGroupByDataToPieChartDataParams = {
|
||||
groupByData: Record<string, GroupByRawResult[]> | null | undefined;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
configuration: PieChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
userTimezone: string;
|
||||
@@ -46,7 +39,6 @@ const EMPTY_PIE_CHART_RESULT: TransformGroupByDataToPieChartDataResult = {
|
||||
export const transformGroupByDataToPieChartData = ({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
userTimezone,
|
||||
@@ -77,61 +69,59 @@ export const transformGroupByDataToPieChartData = ({
|
||||
return EMPTY_PIE_CHART_RESULT;
|
||||
}
|
||||
|
||||
const isDateField = isFieldMetadataDateKind(groupByField.type);
|
||||
const isNestedDateField = isRelationNestedFieldDateKind({
|
||||
relationField: groupByField,
|
||||
relationNestedFieldName: configuration.groupBySubFieldName ?? undefined,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
const dateGranularity: ObjectRecordGroupByDateGranularity | undefined =
|
||||
isDateField || isNestedDateField
|
||||
? (configuration.dateGranularity ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const filteredResults = configuration.hideEmptyCategory
|
||||
? rawResults.filter((result) =>
|
||||
isDefined(result.groupByDimensionValues?.[0]),
|
||||
)
|
||||
: rawResults;
|
||||
|
||||
const { processedDataPoints, formattedToRawLookup } =
|
||||
processOneDimensionalGroupByResults({
|
||||
rawResults: filteredResults,
|
||||
groupByFieldX: groupByField,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName: configuration.groupBySubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
const limitedResults = filteredResults.slice(
|
||||
const limitedProcessedDataPoints = processedDataPoints.slice(
|
||||
0,
|
||||
PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
|
||||
);
|
||||
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: limitedResults,
|
||||
primaryAxisGroupByField: groupByField,
|
||||
primaryAxisDateGranularity: dateGranularity,
|
||||
primaryAxisGroupBySubFieldName:
|
||||
configuration.groupBySubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
type PieChartDataItemWithRawValue = PieChartDataItem & {
|
||||
rawValue: string | null | undefined;
|
||||
};
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
|
||||
const data: PieChartDataItem[] = limitedResults.map((result, index) => {
|
||||
const id = formattedValues[index]?.formattedPrimaryDimensionValue ?? '';
|
||||
|
||||
const value = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
value,
|
||||
const unsortedDataWithRawValues: PieChartDataItemWithRawValue[] =
|
||||
limitedProcessedDataPoints.map(({ xValue, rawXValue, aggregateValue }) => ({
|
||||
id: xValue,
|
||||
value: aggregateValue,
|
||||
color: (configuration.color ?? GRAPH_DEFAULT_COLOR) as GraphColor,
|
||||
};
|
||||
rawValue: isDefined(rawXValue) ? String(rawXValue) : null,
|
||||
}));
|
||||
|
||||
const sortedDataWithRawValues = sortChartData({
|
||||
data: unsortedDataWithRawValues,
|
||||
orderBy: configuration.orderBy ?? undefined,
|
||||
manualSortOrder: configuration.manualSortOrder ?? undefined,
|
||||
formattedToRawLookup,
|
||||
getFieldValue: (item) => item.id,
|
||||
getNumericValue: (item) => item.value,
|
||||
selectFieldOptions: isFieldMetadataSelectKind(groupByField.type)
|
||||
? groupByField.options
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const data: PieChartDataItem[] = sortedDataWithRawValues.map(
|
||||
({ rawValue: _rawValue, ...item }) => item,
|
||||
);
|
||||
|
||||
const showLegend = configuration.displayLegend ?? true;
|
||||
|
||||
return {
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import { sortByManualOrder } from '@/page-layout/widgets/graph/utils/sortByManualOrder';
|
||||
|
||||
describe('sortByManualOrder', () => {
|
||||
describe('basic sorting', () => {
|
||||
it('should sort items according to manual order', () => {
|
||||
const items = [
|
||||
{ id: 'c', value: 3 },
|
||||
{ id: 'a', value: 1 },
|
||||
{ id: 'b', value: 2 },
|
||||
];
|
||||
const manualSortOrder = ['a', 'b', 'c'];
|
||||
|
||||
const result = sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder,
|
||||
getRawValue: (item) => item.id,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should handle reverse order', () => {
|
||||
const items = [
|
||||
{ id: 'a', value: 1 },
|
||||
{ id: 'b', value: 2 },
|
||||
{ id: 'c', value: 3 },
|
||||
];
|
||||
const manualSortOrder = ['c', 'b', 'a'];
|
||||
|
||||
const result = sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder,
|
||||
getRawValue: (item) => item.id,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual(['c', 'b', 'a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty manual sort order', () => {
|
||||
it('should return items unchanged when manual order is empty', () => {
|
||||
const items = [
|
||||
{ id: 'c', value: 3 },
|
||||
{ id: 'a', value: 1 },
|
||||
{ id: 'b', value: 2 },
|
||||
];
|
||||
|
||||
const result = sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder: [],
|
||||
getRawValue: (item) => item.id,
|
||||
});
|
||||
|
||||
expect(result).toEqual(items);
|
||||
});
|
||||
});
|
||||
|
||||
describe('items not in manual order', () => {
|
||||
it('should place items not in manual order at the end', () => {
|
||||
const items = [
|
||||
{ id: 'unknown', value: 0 },
|
||||
{ id: 'b', value: 2 },
|
||||
{ id: 'a', value: 1 },
|
||||
];
|
||||
const manualSortOrder = ['a', 'b'];
|
||||
|
||||
const result = sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder,
|
||||
getRawValue: (item) => item.id,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual(['a', 'b', 'unknown']);
|
||||
});
|
||||
|
||||
it('should maintain relative order of items not in manual order', () => {
|
||||
const items = [
|
||||
{ id: 'unknown1', value: 0 },
|
||||
{ id: 'a', value: 1 },
|
||||
{ id: 'unknown2', value: 0 },
|
||||
];
|
||||
const manualSortOrder = ['a'];
|
||||
|
||||
const result = sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder,
|
||||
getRawValue: (item) => item.id,
|
||||
});
|
||||
|
||||
expect(result[0].id).toBe('a');
|
||||
expect(result[1].id).toBe('unknown1');
|
||||
expect(result[2].id).toBe('unknown2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('null and undefined values', () => {
|
||||
it('should handle null raw values', () => {
|
||||
const items = [
|
||||
{ id: null as string | null, value: 0 },
|
||||
{ id: 'a', value: 1 },
|
||||
];
|
||||
|
||||
const result = sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder: ['a'],
|
||||
getRawValue: (item) => item.id,
|
||||
});
|
||||
|
||||
expect(result[0].id).toBe('a');
|
||||
});
|
||||
|
||||
it('should handle undefined raw values', () => {
|
||||
const items = [
|
||||
{ id: undefined as string | undefined, value: 0 },
|
||||
{ id: 'a', value: 1 },
|
||||
];
|
||||
|
||||
const result = sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder: ['a'],
|
||||
getRawValue: (item) => item.id,
|
||||
});
|
||||
|
||||
expect(result[0].id).toBe('a');
|
||||
});
|
||||
});
|
||||
|
||||
describe('immutability', () => {
|
||||
it('should not mutate the original array', () => {
|
||||
const items = [
|
||||
{ id: 'c', value: 3 },
|
||||
{ id: 'a', value: 1 },
|
||||
{ id: 'b', value: 2 },
|
||||
];
|
||||
const originalItems = [...items];
|
||||
|
||||
sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder: ['a', 'b', 'c'],
|
||||
getRawValue: (item) => item.id,
|
||||
});
|
||||
|
||||
expect(items).toEqual(originalItems);
|
||||
});
|
||||
});
|
||||
});
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { sortBySelectOptionPosition } from '@/page-layout/widgets/graph/utils/sortBySelectOptionPosition';
|
||||
|
||||
describe('sortBySelectOptionPosition', () => {
|
||||
const mockOptions: FieldMetadataItemOption[] = [
|
||||
{ id: '1', value: 'NEW', label: 'New', position: 0, color: 'red' },
|
||||
{
|
||||
id: '2',
|
||||
value: 'IN_PROGRESS',
|
||||
label: 'In Progress',
|
||||
position: 1,
|
||||
color: 'yellow',
|
||||
},
|
||||
{ id: '3', value: 'DONE', label: 'Done', position: 2, color: 'green' },
|
||||
{
|
||||
id: '4',
|
||||
value: 'ARCHIVED',
|
||||
label: 'Archived',
|
||||
position: 3,
|
||||
color: 'gray',
|
||||
},
|
||||
];
|
||||
|
||||
const buildFormattedToRawLookup = (
|
||||
values: { formatted: string; raw: string }[],
|
||||
): Map<string, RawDimensionValue> => {
|
||||
const lookup = new Map<string, RawDimensionValue>();
|
||||
values.forEach(({ formatted, raw }) => {
|
||||
lookup.set(formatted, raw as RawDimensionValue);
|
||||
});
|
||||
return lookup;
|
||||
};
|
||||
|
||||
describe('ASC direction', () => {
|
||||
it('should sort items by select option position in ascending order', () => {
|
||||
const items = [
|
||||
{ id: 'Done', value: 10 },
|
||||
{ id: 'New', value: 5 },
|
||||
{ id: 'In Progress', value: 8 },
|
||||
];
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup([
|
||||
{ formatted: 'Done', raw: 'DONE' },
|
||||
{ formatted: 'New', raw: 'NEW' },
|
||||
{ formatted: 'In Progress', raw: 'IN_PROGRESS' },
|
||||
]);
|
||||
|
||||
const result = sortBySelectOptionPosition({
|
||||
items,
|
||||
options: mockOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: (item) => item.id,
|
||||
direction: 'ASC',
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual([
|
||||
'New',
|
||||
'In Progress',
|
||||
'Done',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should place items with unknown options at the end when ascending', () => {
|
||||
const items = [
|
||||
{ id: 'Unknown', value: 1 },
|
||||
{ id: 'Done', value: 10 },
|
||||
{ id: 'New', value: 5 },
|
||||
];
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup([
|
||||
{ formatted: 'Unknown', raw: 'UNKNOWN_VALUE' },
|
||||
{ formatted: 'Done', raw: 'DONE' },
|
||||
{ formatted: 'New', raw: 'NEW' },
|
||||
]);
|
||||
|
||||
const result = sortBySelectOptionPosition({
|
||||
items,
|
||||
options: mockOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: (item) => item.id,
|
||||
direction: 'ASC',
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual(['New', 'Done', 'Unknown']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DESC direction', () => {
|
||||
it('should sort items by select option position in descending order', () => {
|
||||
const items = [
|
||||
{ id: 'New', value: 5 },
|
||||
{ id: 'Done', value: 10 },
|
||||
{ id: 'In Progress', value: 8 },
|
||||
];
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup([
|
||||
{ formatted: 'New', raw: 'NEW' },
|
||||
{ formatted: 'Done', raw: 'DONE' },
|
||||
{ formatted: 'In Progress', raw: 'IN_PROGRESS' },
|
||||
]);
|
||||
|
||||
const result = sortBySelectOptionPosition({
|
||||
items,
|
||||
options: mockOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: (item) => item.id,
|
||||
direction: 'DESC',
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual([
|
||||
'Done',
|
||||
'In Progress',
|
||||
'New',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should place items with unknown options at the beginning when descending', () => {
|
||||
const items = [
|
||||
{ id: 'New', value: 5 },
|
||||
{ id: 'Unknown', value: 1 },
|
||||
{ id: 'Done', value: 10 },
|
||||
];
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup([
|
||||
{ formatted: 'New', raw: 'NEW' },
|
||||
{ formatted: 'Unknown', raw: 'UNKNOWN_VALUE' },
|
||||
{ formatted: 'Done', raw: 'DONE' },
|
||||
]);
|
||||
|
||||
const result = sortBySelectOptionPosition({
|
||||
items,
|
||||
options: mockOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: (item) => item.id,
|
||||
direction: 'DESC',
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual(['Unknown', 'Done', 'New']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty items array', () => {
|
||||
const formattedToRawLookup = new Map<string, RawDimensionValue>();
|
||||
|
||||
const result = sortBySelectOptionPosition({
|
||||
items: [],
|
||||
options: mockOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: (item: { id: string }) => item.id,
|
||||
direction: 'ASC',
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle items not in lookup', () => {
|
||||
const items = [
|
||||
{ id: 'NotInLookup', value: 5 },
|
||||
{ id: 'New', value: 10 },
|
||||
];
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup([
|
||||
{ formatted: 'New', raw: 'NEW' },
|
||||
]);
|
||||
|
||||
const result = sortBySelectOptionPosition({
|
||||
items,
|
||||
options: mockOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: (item) => item.id,
|
||||
direction: 'ASC',
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.id)).toEqual(['New', 'NotInLookup']);
|
||||
});
|
||||
|
||||
it('should not mutate the original array', () => {
|
||||
const items = [
|
||||
{ id: 'Done', value: 10 },
|
||||
{ id: 'New', value: 5 },
|
||||
];
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup([
|
||||
{ formatted: 'Done', raw: 'DONE' },
|
||||
{ formatted: 'New', raw: 'NEW' },
|
||||
]);
|
||||
|
||||
const originalItems = [...items];
|
||||
|
||||
sortBySelectOptionPosition({
|
||||
items,
|
||||
options: mockOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: (item) => item.id,
|
||||
direction: 'ASC',
|
||||
});
|
||||
|
||||
expect(items).toEqual(originalItems);
|
||||
});
|
||||
});
|
||||
});
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { sortChartData } from '@/page-layout/widgets/graph/utils/sortChartData';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
describe('sortChartData', () => {
|
||||
type TestDataPoint = { label: string; value: number };
|
||||
|
||||
const testData: TestDataPoint[] = [
|
||||
{ label: 'Beta', value: 30 },
|
||||
{ label: 'Alpha', value: 10 },
|
||||
{ label: 'Gamma', value: 20 },
|
||||
];
|
||||
|
||||
const formattedToRawLookup = new Map<string, RawDimensionValue>([
|
||||
['Alpha', 'ALPHA'],
|
||||
['Beta', 'BETA'],
|
||||
['Gamma', 'GAMMA'],
|
||||
]);
|
||||
|
||||
const getFieldValue = (item: TestDataPoint) => item.label;
|
||||
const getNumericValue = (item: TestDataPoint) => item.value;
|
||||
|
||||
describe('null or undefined orderBy', () => {
|
||||
it('should return data unchanged when orderBy is null', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: null,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should return data unchanged when orderBy is undefined', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: undefined,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FIELD_ASC sorting', () => {
|
||||
it('should sort by field value ascending', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.label)).toEqual([
|
||||
'Alpha',
|
||||
'Beta',
|
||||
'Gamma',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FIELD_DESC sorting', () => {
|
||||
it('should sort by field value descending', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.label)).toEqual([
|
||||
'Gamma',
|
||||
'Beta',
|
||||
'Alpha',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VALUE_ASC sorting', () => {
|
||||
it('should sort by numeric value ascending', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.VALUE_ASC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.value)).toEqual([10, 20, 30]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VALUE_DESC sorting', () => {
|
||||
it('should sort by numeric value descending', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.value)).toEqual([30, 20, 10]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MANUAL sorting', () => {
|
||||
it('should sort by manual order', () => {
|
||||
const manualSortOrder = ['GAMMA', 'ALPHA', 'BETA'];
|
||||
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.MANUAL,
|
||||
manualSortOrder,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.label)).toEqual([
|
||||
'Gamma',
|
||||
'Alpha',
|
||||
'Beta',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return data unchanged when manual order is undefined', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.MANUAL,
|
||||
manualSortOrder: undefined,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should return data unchanged when manual order is null', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.MANUAL,
|
||||
manualSortOrder: null,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FIELD_POSITION_ASC sorting', () => {
|
||||
const selectFieldOptions: FieldMetadataItemOption[] = [
|
||||
{ id: '1', value: 'ALPHA', label: 'Alpha', position: 2, color: 'blue' },
|
||||
{ id: '2', value: 'BETA', label: 'Beta', position: 0, color: 'red' },
|
||||
{ id: '3', value: 'GAMMA', label: 'Gamma', position: 1, color: 'green' },
|
||||
];
|
||||
|
||||
it('should sort by select option position ascending', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
selectFieldOptions,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.label)).toEqual([
|
||||
'Beta',
|
||||
'Gamma',
|
||||
'Alpha',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw error when select options are not provided', () => {
|
||||
expect(() =>
|
||||
sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
}),
|
||||
).toThrow('Select field options are required');
|
||||
});
|
||||
|
||||
it('should throw error when select options are empty', () => {
|
||||
expect(() =>
|
||||
sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.FIELD_POSITION_ASC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
selectFieldOptions: [],
|
||||
}),
|
||||
).toThrow('Select field options are required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FIELD_POSITION_DESC sorting', () => {
|
||||
const selectFieldOptions: FieldMetadataItemOption[] = [
|
||||
{ id: '1', value: 'ALPHA', label: 'Alpha', position: 2, color: 'blue' },
|
||||
{ id: '2', value: 'BETA', label: 'Beta', position: 0, color: 'red' },
|
||||
{ id: '3', value: 'GAMMA', label: 'Gamma', position: 1, color: 'green' },
|
||||
];
|
||||
|
||||
it('should sort by select option position descending', () => {
|
||||
const result = sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.FIELD_POSITION_DESC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
selectFieldOptions,
|
||||
});
|
||||
|
||||
expect(result.map((item) => item.label)).toEqual([
|
||||
'Alpha',
|
||||
'Gamma',
|
||||
'Beta',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('immutability', () => {
|
||||
it('should not mutate the original data array', () => {
|
||||
const originalData = [...testData];
|
||||
|
||||
sortChartData({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
});
|
||||
|
||||
expect(testData).toEqual(originalData);
|
||||
});
|
||||
});
|
||||
});
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { sortOptionsForManualOrder } from '@/page-layout/widgets/graph/utils/sortOptionsForManualOrder';
|
||||
|
||||
describe('sortOptionsForManualOrder', () => {
|
||||
describe('without manual sort order', () => {
|
||||
it('should sort by position when no manual order provided', () => {
|
||||
const options = [
|
||||
{ value: 'c', position: 2 },
|
||||
{ value: 'a', position: 0 },
|
||||
{ value: 'b', position: 1 },
|
||||
];
|
||||
|
||||
const result = sortOptionsForManualOrder(options, undefined);
|
||||
|
||||
expect(result.map((option) => option.value)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should sort by position when manual order is null', () => {
|
||||
const options = [
|
||||
{ value: 'c', position: 2 },
|
||||
{ value: 'a', position: 0 },
|
||||
{ value: 'b', position: 1 },
|
||||
];
|
||||
|
||||
const result = sortOptionsForManualOrder(options, null);
|
||||
|
||||
expect(result.map((option) => option.value)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should sort by position when manual order is empty', () => {
|
||||
const options = [
|
||||
{ value: 'c', position: 2 },
|
||||
{ value: 'a', position: 0 },
|
||||
{ value: 'b', position: 1 },
|
||||
];
|
||||
|
||||
const result = sortOptionsForManualOrder(options, []);
|
||||
|
||||
expect(result.map((option) => option.value)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should handle null positions as 0', () => {
|
||||
const options = [
|
||||
{ value: 'b', position: 1 },
|
||||
{ value: 'a', position: null },
|
||||
];
|
||||
|
||||
const result = sortOptionsForManualOrder(options, undefined);
|
||||
|
||||
expect(result.map((option) => option.value)).toEqual(['a', 'b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with manual sort order', () => {
|
||||
it('should sort according to manual order', () => {
|
||||
const options = [
|
||||
{ value: 'a', position: 0 },
|
||||
{ value: 'b', position: 1 },
|
||||
{ value: 'c', position: 2 },
|
||||
];
|
||||
const manualSortOrder = ['c', 'a', 'b'];
|
||||
|
||||
const result = sortOptionsForManualOrder(options, manualSortOrder);
|
||||
|
||||
expect(result.map((option) => option.value)).toEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('should place options not in manual order at the end', () => {
|
||||
const options = [
|
||||
{ value: 'a', position: 0 },
|
||||
{ value: 'b', position: 1 },
|
||||
{ value: 'c', position: 2 },
|
||||
];
|
||||
const manualSortOrder = ['b'];
|
||||
|
||||
const result = sortOptionsForManualOrder(options, manualSortOrder);
|
||||
|
||||
expect(result.map((option) => option.value)).toEqual(['b', 'a', 'c']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('immutability', () => {
|
||||
it('should not mutate the original array', () => {
|
||||
const options = [
|
||||
{ value: 'c', position: 2 },
|
||||
{ value: 'a', position: 0 },
|
||||
];
|
||||
const originalOptions = [...options];
|
||||
|
||||
sortOptionsForManualOrder(options, ['a', 'c']);
|
||||
|
||||
expect(options).toEqual(originalOptions);
|
||||
});
|
||||
});
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { sortSecondaryAxisData } from '@/page-layout/widgets/graph/utils/sortSecondaryAxisData';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type TestItem = { label: string };
|
||||
|
||||
const testItems: TestItem[] = [
|
||||
{ label: 'Beta' },
|
||||
{ label: 'Alpha' },
|
||||
{ label: 'Gamma' },
|
||||
];
|
||||
const getFormattedValue = (item: TestItem) => item.label;
|
||||
|
||||
describe('sortSecondaryAxisData', () => {
|
||||
it('should sort by FIELD_ASC', () => {
|
||||
const result = sortSecondaryAxisData({
|
||||
items: testItems,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
getFormattedValue,
|
||||
});
|
||||
|
||||
expect(result.map((i) => i.label)).toEqual(['Alpha', 'Beta', 'Gamma']);
|
||||
});
|
||||
|
||||
it('should sort by FIELD_DESC', () => {
|
||||
const result = sortSecondaryAxisData({
|
||||
items: testItems,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
getFormattedValue,
|
||||
});
|
||||
|
||||
expect(result.map((i) => i.label)).toEqual(['Gamma', 'Beta', 'Alpha']);
|
||||
});
|
||||
|
||||
it('should return items unchanged when orderBy is undefined', () => {
|
||||
const result = sortSecondaryAxisData({
|
||||
items: testItems,
|
||||
orderBy: undefined,
|
||||
getFormattedValue,
|
||||
});
|
||||
|
||||
expect(result).toEqual(testItems);
|
||||
});
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually } from '@/page-layout/widgets/graph/utils/sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type TestItem = { label: string };
|
||||
|
||||
const testData: TestItem[] = [
|
||||
{ label: 'Beta' },
|
||||
{ label: 'Alpha' },
|
||||
{ label: 'Gamma' },
|
||||
];
|
||||
const formattedToRawLookup = new Map([
|
||||
['Alpha', 'ALPHA'],
|
||||
['Beta', 'BETA'],
|
||||
['Gamma', 'GAMMA'],
|
||||
]);
|
||||
const getFormattedValue = (item: TestItem) => item.label;
|
||||
|
||||
describe('sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually', () => {
|
||||
it('should sort by FIELD_ASC', () => {
|
||||
const result = sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue,
|
||||
});
|
||||
|
||||
expect(result.map((i) => i.label)).toEqual(['Alpha', 'Beta', 'Gamma']);
|
||||
});
|
||||
|
||||
it('should sort by MANUAL order', () => {
|
||||
const result = sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually({
|
||||
data: testData,
|
||||
orderBy: GraphOrderBy.MANUAL,
|
||||
manualSortOrder: ['GAMMA', 'ALPHA', 'BETA'],
|
||||
formattedToRawLookup,
|
||||
getFormattedValue,
|
||||
});
|
||||
|
||||
expect(result.map((i) => i.label)).toEqual(['Gamma', 'Alpha', 'Beta']);
|
||||
});
|
||||
|
||||
it('should return data unchanged when orderBy is undefined', () => {
|
||||
const result = sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually({
|
||||
data: testData,
|
||||
orderBy: undefined,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue,
|
||||
});
|
||||
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
});
|
||||
+30
-27
@@ -122,19 +122,21 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
|
||||
| ObjectRecordOrderByForRelationField
|
||||
> = [];
|
||||
|
||||
orderBy.push(
|
||||
getGroupByOrderBy({
|
||||
graphOrderBy:
|
||||
chartConfiguration.primaryAxisOrderBy ?? GRAPH_DEFAULT_ORDER_BY,
|
||||
groupByField: groupByFieldX,
|
||||
groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: shouldApplyDateGranularityX
|
||||
? (chartConfiguration.primaryAxisDateGranularity ??
|
||||
GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
const primaryAxisOrderBy = getGroupByOrderBy({
|
||||
graphOrderBy:
|
||||
chartConfiguration.primaryAxisOrderBy ?? GRAPH_DEFAULT_ORDER_BY,
|
||||
groupByField: groupByFieldX,
|
||||
groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: shouldApplyDateGranularityX
|
||||
? (chartConfiguration.primaryAxisDateGranularity ??
|
||||
GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (isDefined(primaryAxisOrderBy)) {
|
||||
orderBy.push(primaryAxisOrderBy);
|
||||
}
|
||||
|
||||
if (isDefined(groupByFieldY)) {
|
||||
const isFieldYDateForOrderBy = isFieldMetadataDateKind(groupByFieldY.type);
|
||||
@@ -148,20 +150,21 @@ export const generateGroupByQueryVariablesFromBarOrLineChartConfiguration = ({
|
||||
const shouldApplyDateGranularityYForOrderBy =
|
||||
isFieldYDateForOrderBy || isFieldYNestedDateForOrderBy;
|
||||
|
||||
orderBy.push(
|
||||
getGroupByOrderBy({
|
||||
graphOrderBy:
|
||||
chartConfiguration.secondaryAxisOrderBy ?? GRAPH_DEFAULT_ORDER_BY,
|
||||
groupByField: groupByFieldY,
|
||||
groupBySubFieldName:
|
||||
chartConfiguration.secondaryAxisGroupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: shouldApplyDateGranularityYForOrderBy
|
||||
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
|
||||
GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
const secondaryAxisOrderBy = getGroupByOrderBy({
|
||||
graphOrderBy:
|
||||
chartConfiguration.secondaryAxisOrderBy ?? GRAPH_DEFAULT_ORDER_BY,
|
||||
groupByField: groupByFieldY,
|
||||
groupBySubFieldName: chartConfiguration.secondaryAxisGroupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: shouldApplyDateGranularityYForOrderBy
|
||||
? (chartConfiguration.secondaryAxisGroupByDateGranularity ??
|
||||
GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (isDefined(secondaryAxisOrderBy)) {
|
||||
orderBy.push(secondaryAxisOrderBy);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+13
-11
@@ -80,17 +80,19 @@ export const generateGroupByQueryVariablesFromPieChartConfiguration = ({
|
||||
> = [];
|
||||
|
||||
if (isDefined(chartConfiguration.orderBy)) {
|
||||
orderBy.push(
|
||||
getGroupByOrderBy({
|
||||
graphOrderBy: chartConfiguration.orderBy,
|
||||
groupByField,
|
||||
groupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: shouldApplyDateGranularity
|
||||
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
const orderByItem = getGroupByOrderBy({
|
||||
graphOrderBy: chartConfiguration.orderBy,
|
||||
groupByField,
|
||||
groupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity: shouldApplyDateGranularity
|
||||
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (isDefined(orderByItem)) {
|
||||
orderBy.push(orderByItem);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+6
-1
@@ -29,7 +29,8 @@ export const getGroupByOrderBy = ({
|
||||
| ObjectRecordOrderByForScalarField
|
||||
| ObjectRecordOrderByWithGroupByDateField
|
||||
| ObjectRecordOrderByForCompositeField
|
||||
| ObjectRecordOrderByForRelationField => {
|
||||
| ObjectRecordOrderByForRelationField
|
||||
| undefined => {
|
||||
switch (graphOrderBy) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
@@ -53,6 +54,10 @@ export const getGroupByOrderBy = ({
|
||||
},
|
||||
};
|
||||
}
|
||||
case GraphOrderBy.FIELD_POSITION_ASC:
|
||||
case GraphOrderBy.FIELD_POSITION_DESC:
|
||||
case GraphOrderBy.MANUAL:
|
||||
return undefined;
|
||||
default:
|
||||
assertUnreachable(graphOrderBy);
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const getSortedKeys = ({
|
||||
orderByY,
|
||||
yValues,
|
||||
}: {
|
||||
orderByY?: GraphOrderBy | null;
|
||||
yValues: string[];
|
||||
}) => {
|
||||
switch (orderByY) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return Array.from(yValues).sort((a, b) => a.localeCompare(b));
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return Array.from(yValues).sort((a, b) => b.localeCompare(a));
|
||||
default:
|
||||
return Array.from(yValues);
|
||||
}
|
||||
};
|
||||
+6
-1
@@ -3,7 +3,11 @@ import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const mapOrderByToDirection = (
|
||||
orderByEnum: GraphOrderBy,
|
||||
orderByEnum:
|
||||
| GraphOrderBy.FIELD_ASC
|
||||
| GraphOrderBy.FIELD_DESC
|
||||
| GraphOrderBy.VALUE_ASC
|
||||
| GraphOrderBy.VALUE_DESC,
|
||||
): OrderByDirection => {
|
||||
switch (orderByEnum) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
@@ -14,6 +18,7 @@ export const mapOrderByToDirection = (
|
||||
return OrderByDirection.AscNullsLast;
|
||||
case GraphOrderBy.VALUE_DESC:
|
||||
return OrderByDirection.DescNullsLast;
|
||||
|
||||
default:
|
||||
assertUnreachable(orderByEnum);
|
||||
}
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { type FirstDayOfTheWeek, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type OneDimensionalChartConfiguration = {
|
||||
primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null;
|
||||
primaryAxisGroupBySubFieldName?: string | null;
|
||||
aggregateOperation: string;
|
||||
};
|
||||
|
||||
type ProcessOneDimensionalGroupByResultsParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
groupByFieldX: FieldMetadataItem;
|
||||
aggregateField: FieldMetadataItem;
|
||||
configuration: OneDimensionalChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
primaryAxisSubFieldName?: string | null;
|
||||
userTimezone: string;
|
||||
firstDayOfTheWeek: FirstDayOfTheWeek;
|
||||
};
|
||||
|
||||
export type ProcessedOneDimensionalDataPoint = {
|
||||
xValue: string;
|
||||
rawXValue: RawDimensionValue;
|
||||
aggregateValue: number;
|
||||
};
|
||||
|
||||
export type ProcessOneDimensionalGroupByResultsOutput = {
|
||||
processedDataPoints: ProcessedOneDimensionalDataPoint[];
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
export const processOneDimensionalGroupByResults = ({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
}: ProcessOneDimensionalGroupByResultsParams): ProcessOneDimensionalGroupByResultsOutput => {
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: rawResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
primaryAxisDateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
const processedDataPoints: ProcessedOneDimensionalDataPoint[] = [];
|
||||
|
||||
for (const result of rawResults) {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
|
||||
if (!isDefined(dimensionValues) || dimensionValues.length < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawXValue = dimensionValues[0] as RawDimensionValue;
|
||||
|
||||
const xValue = formatDimensionValue({
|
||||
value: rawXValue,
|
||||
fieldMetadata: groupByFieldX,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity ?? undefined,
|
||||
subFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
if (isDefined(rawXValue)) {
|
||||
formattedToRawLookup.set(xValue, rawXValue);
|
||||
}
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (!isDefined(aggregateValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedDataPoints.push({
|
||||
xValue,
|
||||
rawXValue,
|
||||
aggregateValue,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
processedDataPoints,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
+57
-82
@@ -1,27 +1,29 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData';
|
||||
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { sortLineChartSeries } from '@/page-layout/widgets/graph/utils/sortLineChartSeries';
|
||||
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { type FirstDayOfTheWeek, isDefined } from 'twenty-shared/utils';
|
||||
import { type LineChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformTwoDimensionalGroupByToLineChartDataParams = {
|
||||
type TwoDimensionalChartConfiguration = {
|
||||
primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null;
|
||||
primaryAxisGroupBySubFieldName?: string | null;
|
||||
secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null;
|
||||
secondaryAxisGroupBySubFieldName?: string | null;
|
||||
aggregateOperation: string;
|
||||
};
|
||||
|
||||
type ProcessTwoDimensionalGroupByResultsParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
groupByFieldX: FieldMetadataItem;
|
||||
groupByFieldY: FieldMetadataItem;
|
||||
aggregateField: FieldMetadataItem;
|
||||
configuration: LineChartConfiguration;
|
||||
configuration: TwoDimensionalChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
primaryAxisSubFieldName?: string | null;
|
||||
@@ -29,13 +31,21 @@ type TransformTwoDimensionalGroupByToLineChartDataParams = {
|
||||
firstDayOfTheWeek: FirstDayOfTheWeek;
|
||||
};
|
||||
|
||||
type TransformTwoDimensionalGroupByToLineChartDataResult = {
|
||||
series: LineChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
export type ProcessedTwoDimensionalDataPoint = {
|
||||
xValue: string;
|
||||
yValue: string;
|
||||
rawXValue: RawDimensionValue;
|
||||
rawYValue: RawDimensionValue;
|
||||
aggregateValue: number;
|
||||
};
|
||||
|
||||
export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
export type ProcessTwoDimensionalGroupByResultsOutput = {
|
||||
processedDataPoints: ProcessedTwoDimensionalDataPoint[];
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
yFormattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
export const processTwoDimensionalGroupByResults = ({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
@@ -46,11 +56,7 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
primaryAxisSubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
}: TransformTwoDimensionalGroupByToLineChartDataParams): TransformTwoDimensionalGroupByToLineChartDataResult => {
|
||||
const seriesMap = new Map<string, Map<string, number>>();
|
||||
const allXValues: string[] = [];
|
||||
const xValueSet = new Set<string>();
|
||||
|
||||
}: ProcessTwoDimensionalGroupByResultsParams): ProcessTwoDimensionalGroupByResultsOutput => {
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: rawResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
@@ -62,17 +68,21 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
});
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
let hasTooManyGroups = false;
|
||||
const yFormattedToRawLookup = new Map<string, RawDimensionValue>();
|
||||
const processedDataPoints: ProcessedTwoDimensionalDataPoint[] = [];
|
||||
|
||||
rawResults.forEach((result) => {
|
||||
for (const result of rawResults) {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
if (!isDefined(dimensionValues) || dimensionValues.length < 2) return;
|
||||
|
||||
const rawAggregateValue = result[aggregateOperation];
|
||||
if (!isDefined(rawAggregateValue)) return;
|
||||
if (!isDefined(dimensionValues) || dimensionValues.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawXValue = dimensionValues[0] as RawDimensionValue;
|
||||
const rawYValue = dimensionValues[1] as RawDimensionValue;
|
||||
|
||||
const xValue = formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
value: rawXValue,
|
||||
fieldMetadata: groupByFieldX,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity ?? undefined,
|
||||
subFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
@@ -80,26 +90,8 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
// TODO: Add a limit to the query instead of checking here (issue: twentyhq/core-team-issues#1600)
|
||||
const isNewX = !xValueSet.has(xValue);
|
||||
|
||||
if (
|
||||
isNewX &&
|
||||
xValueSet.size >= LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS
|
||||
) {
|
||||
hasTooManyGroups = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNewX) {
|
||||
xValueSet.add(xValue);
|
||||
allXValues.push(xValue);
|
||||
}
|
||||
|
||||
const seriesRawValue = dimensionValues[1];
|
||||
|
||||
const seriesKey = formatDimensionValue({
|
||||
value: seriesRawValue,
|
||||
const yValue = formatDimensionValue({
|
||||
value: rawYValue,
|
||||
fieldMetadata: groupByFieldY,
|
||||
dateGranularity:
|
||||
configuration.secondaryAxisGroupByDateGranularity ?? undefined,
|
||||
@@ -108,6 +100,14 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
firstDayOfTheWeek,
|
||||
});
|
||||
|
||||
if (isDefined(rawXValue)) {
|
||||
formattedToRawLookup.set(xValue, rawXValue);
|
||||
}
|
||||
|
||||
if (isDefined(rawYValue)) {
|
||||
yFormattedToRawLookup.set(yValue, rawYValue);
|
||||
}
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
@@ -117,47 +117,22 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (!isDefined(aggregateValue)) return;
|
||||
|
||||
if (!seriesMap.has(seriesKey)) {
|
||||
seriesMap.set(seriesKey, new Map());
|
||||
if (!isDefined(aggregateValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seriesMap.get(seriesKey)!.set(xValue, aggregateValue);
|
||||
});
|
||||
|
||||
const unsortedSeries: LineChartSeries[] = Array.from(seriesMap.entries()).map(
|
||||
([seriesKey, xToYMap]) => {
|
||||
const data: LineChartDataPoint[] = allXValues.map((xValue) => ({
|
||||
x: xValue,
|
||||
y: xToYMap.get(xValue) ?? 0,
|
||||
}));
|
||||
|
||||
const transformedData = configuration.isCumulative
|
||||
? applyCumulativeTransformToLineChartData({
|
||||
data,
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
})
|
||||
: data;
|
||||
|
||||
return {
|
||||
id: seriesKey,
|
||||
label: seriesKey,
|
||||
color: configuration.color as GraphColor,
|
||||
data: transformedData,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const series = sortLineChartSeries({
|
||||
series: unsortedSeries,
|
||||
orderByY: configuration.secondaryAxisOrderBy,
|
||||
});
|
||||
processedDataPoints.push({
|
||||
xValue,
|
||||
yValue,
|
||||
rawXValue,
|
||||
rawYValue,
|
||||
aggregateValue,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
series,
|
||||
hasTooManyGroups,
|
||||
processedDataPoints,
|
||||
formattedToRawLookup,
|
||||
yFormattedToRawLookup,
|
||||
};
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SortByManualOrderParams<T> = {
|
||||
items: T[];
|
||||
manualSortOrder: string[];
|
||||
getRawValue: (item: T) => string | null | undefined;
|
||||
};
|
||||
|
||||
export const sortByManualOrder = <T>({
|
||||
items,
|
||||
manualSortOrder,
|
||||
getRawValue,
|
||||
}: SortByManualOrderParams<T>): T[] => {
|
||||
if (manualSortOrder.length === 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const orderMap = new Map(
|
||||
manualSortOrder.map((value, index) => [value, index]),
|
||||
);
|
||||
|
||||
return items.toSorted((a, b) => {
|
||||
const rawValueA = getRawValue(a) ?? '';
|
||||
const rawValueB = getRawValue(b) ?? '';
|
||||
|
||||
const indexA = orderMap.get(rawValueA);
|
||||
const indexB = orderMap.get(rawValueB);
|
||||
|
||||
if (!isDefined(indexA) && !isDefined(indexB)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!isDefined(indexA)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!isDefined(indexB)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return indexA - indexB;
|
||||
});
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SortBySelectOptionPositionParams<T> = {
|
||||
items: T[];
|
||||
options: FieldMetadataItemOption[];
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
getFormattedValue: (item: T) => string;
|
||||
direction: 'ASC' | 'DESC';
|
||||
};
|
||||
|
||||
export const sortBySelectOptionPosition = <T>({
|
||||
items,
|
||||
options,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue,
|
||||
direction,
|
||||
}: SortBySelectOptionPositionParams<T>): T[] => {
|
||||
const optionValueToPosition = new Map<string, number>();
|
||||
|
||||
for (const option of options) {
|
||||
optionValueToPosition.set(option.value, option.position);
|
||||
}
|
||||
|
||||
return items.toSorted((a, b) => {
|
||||
const formattedA = getFormattedValue(a);
|
||||
const formattedB = getFormattedValue(b);
|
||||
|
||||
const rawA = formattedToRawLookup.get(formattedA);
|
||||
const rawB = formattedToRawLookup.get(formattedB);
|
||||
|
||||
const positionA = isDefined(rawA)
|
||||
? (optionValueToPosition.get(String(rawA)) ?? Number.MAX_SAFE_INTEGER)
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const positionB = isDefined(rawB)
|
||||
? (optionValueToPosition.get(String(rawB)) ?? Number.MAX_SAFE_INTEGER)
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
|
||||
return direction === 'ASC' ? positionA - positionB : positionB - positionA;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { sortByManualOrder } from '@/page-layout/widgets/graph/utils/sortByManualOrder';
|
||||
import { sortBySelectOptionPosition } from '@/page-layout/widgets/graph/utils/sortBySelectOptionPosition';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type SortChartDataParams<T> = {
|
||||
data: T[];
|
||||
orderBy?: GraphOrderBy | null;
|
||||
manualSortOrder?: string[] | null;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
getFieldValue: (item: T) => string;
|
||||
getNumericValue: (item: T) => number;
|
||||
selectFieldOptions?: FieldMetadataItemOption[] | null;
|
||||
};
|
||||
|
||||
export const sortChartData = <T>({
|
||||
data,
|
||||
orderBy,
|
||||
manualSortOrder,
|
||||
formattedToRawLookup,
|
||||
getFieldValue,
|
||||
getNumericValue,
|
||||
selectFieldOptions,
|
||||
}: SortChartDataParams<T>): T[] => {
|
||||
if (!isDefined(orderBy)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
switch (orderBy) {
|
||||
case GraphOrderBy.MANUAL: {
|
||||
if (!isDefined(manualSortOrder)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return sortByManualOrder({
|
||||
items: data,
|
||||
manualSortOrder,
|
||||
getRawValue: (item) => {
|
||||
const formatted = getFieldValue(item);
|
||||
const raw = formattedToRawLookup.get(formatted);
|
||||
|
||||
return isDefined(raw) ? String(raw) : formatted;
|
||||
},
|
||||
});
|
||||
}
|
||||
case GraphOrderBy.VALUE_ASC:
|
||||
return data.toSorted((a, b) => getNumericValue(a) - getNumericValue(b));
|
||||
case GraphOrderBy.VALUE_DESC:
|
||||
return data.toSorted((a, b) => getNumericValue(b) - getNumericValue(a));
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return data.toSorted((a, b) =>
|
||||
getFieldValue(a).localeCompare(getFieldValue(b)),
|
||||
);
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return data.toSorted((a, b) =>
|
||||
getFieldValue(b).localeCompare(getFieldValue(a)),
|
||||
);
|
||||
case GraphOrderBy.FIELD_POSITION_ASC:
|
||||
if (!isDefined(selectFieldOptions) || selectFieldOptions.length === 0) {
|
||||
throw new Error('Select field options are required');
|
||||
}
|
||||
|
||||
return sortBySelectOptionPosition<T>({
|
||||
items: data,
|
||||
options: selectFieldOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: getFieldValue,
|
||||
direction: 'ASC',
|
||||
});
|
||||
case GraphOrderBy.FIELD_POSITION_DESC:
|
||||
if (!isDefined(selectFieldOptions) || selectFieldOptions.length === 0) {
|
||||
throw new Error('Select field options are required');
|
||||
}
|
||||
|
||||
return sortBySelectOptionPosition<T>({
|
||||
items: data,
|
||||
options: selectFieldOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue: getFieldValue,
|
||||
direction: 'DESC',
|
||||
});
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
};
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const sortLineChartSeries = ({
|
||||
series,
|
||||
orderByY,
|
||||
}: {
|
||||
series: LineChartSeries[];
|
||||
orderByY?: GraphOrderBy | null;
|
||||
}): LineChartSeries[] => {
|
||||
switch (orderByY) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return [...series].sort((a, b) => b.id.localeCompare(a.id));
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return [...series].sort((a, b) => a.id.localeCompare(b.id));
|
||||
default:
|
||||
return series;
|
||||
}
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { sortByManualOrder } from '@/page-layout/widgets/graph/utils/sortByManualOrder';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SelectFieldOption = {
|
||||
value: string;
|
||||
position?: number | null;
|
||||
};
|
||||
|
||||
export const sortOptionsForManualOrder = <T extends SelectFieldOption>(
|
||||
options: T[],
|
||||
manualSortOrder?: string[] | null,
|
||||
): T[] => {
|
||||
if (!isDefined(manualSortOrder) || manualSortOrder.length === 0) {
|
||||
return options.toSorted((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
||||
}
|
||||
|
||||
return sortByManualOrder({
|
||||
items: options.toSorted((a, b) => (a.position ?? 0) - (b.position ?? 0)),
|
||||
manualSortOrder,
|
||||
getRawValue: (option) => option.value,
|
||||
});
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { sortByManualOrder } from '@/page-layout/widgets/graph/utils/sortByManualOrder';
|
||||
import { sortBySelectOptionPosition } from '@/page-layout/widgets/graph/utils/sortBySelectOptionPosition';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type SortSecondaryAxisDataParams<T> = {
|
||||
items: T[];
|
||||
orderBy?: GraphOrderBy | null;
|
||||
manualSortOrder?: string[] | null;
|
||||
formattedToRawLookup?: Map<string, RawDimensionValue>;
|
||||
selectFieldOptions?: FieldMetadataItemOption[] | null;
|
||||
getFormattedValue: (item: T) => string;
|
||||
};
|
||||
|
||||
export const sortSecondaryAxisData = <T>({
|
||||
items,
|
||||
orderBy,
|
||||
manualSortOrder,
|
||||
formattedToRawLookup,
|
||||
selectFieldOptions,
|
||||
getFormattedValue,
|
||||
}: SortSecondaryAxisDataParams<T>): T[] => {
|
||||
switch (orderBy) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return items.toSorted((a, b) =>
|
||||
getFormattedValue(a).localeCompare(getFormattedValue(b)),
|
||||
);
|
||||
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return items.toSorted((a, b) =>
|
||||
getFormattedValue(b).localeCompare(getFormattedValue(a)),
|
||||
);
|
||||
|
||||
case GraphOrderBy.FIELD_POSITION_ASC:
|
||||
case GraphOrderBy.FIELD_POSITION_DESC: {
|
||||
if (
|
||||
!isDefined(selectFieldOptions) ||
|
||||
selectFieldOptions.length === 0 ||
|
||||
!isDefined(formattedToRawLookup)
|
||||
) {
|
||||
throw new Error(
|
||||
'Select field options and formatted to raw lookup are required',
|
||||
);
|
||||
}
|
||||
|
||||
return sortBySelectOptionPosition({
|
||||
items,
|
||||
options: selectFieldOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue,
|
||||
direction: orderBy === GraphOrderBy.FIELD_POSITION_ASC ? 'ASC' : 'DESC',
|
||||
});
|
||||
}
|
||||
|
||||
case GraphOrderBy.MANUAL: {
|
||||
if (!isDefined(manualSortOrder)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return sortByManualOrder({
|
||||
items,
|
||||
manualSortOrder,
|
||||
getRawValue: (item) => {
|
||||
const formattedValue = getFormattedValue(item);
|
||||
const rawValue = formattedToRawLookup?.get(formattedValue);
|
||||
|
||||
return isDefined(rawValue) ? String(rawValue) : formattedValue;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return items;
|
||||
}
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { sortByManualOrder } from '@/page-layout/widgets/graph/utils/sortByManualOrder';
|
||||
import { sortBySelectOptionPosition } from '@/page-layout/widgets/graph/utils/sortBySelectOptionPosition';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type SortTwoDimensionalChartPrimaryAxisDataParams<T> = {
|
||||
data: T[];
|
||||
orderBy?:
|
||||
| GraphOrderBy.FIELD_ASC
|
||||
| GraphOrderBy.FIELD_DESC
|
||||
| GraphOrderBy.FIELD_POSITION_ASC
|
||||
| GraphOrderBy.FIELD_POSITION_DESC
|
||||
| GraphOrderBy.MANUAL;
|
||||
manualSortOrder?: string[] | null;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
getFormattedValue: (item: T) => string;
|
||||
selectFieldOptions?: FieldMetadataItemOption[] | null;
|
||||
};
|
||||
|
||||
export const sortTwoDimensionalChartPrimaryAxisDataByFieldOrManually = <T>({
|
||||
data,
|
||||
orderBy,
|
||||
manualSortOrder,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue,
|
||||
selectFieldOptions,
|
||||
}: SortTwoDimensionalChartPrimaryAxisDataParams<T>): T[] => {
|
||||
if (!isDefined(orderBy)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
switch (orderBy) {
|
||||
case GraphOrderBy.MANUAL: {
|
||||
if (!isDefined(manualSortOrder)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return sortByManualOrder({
|
||||
items: data,
|
||||
manualSortOrder,
|
||||
getRawValue: (item) => {
|
||||
const formattedValue = getFormattedValue(item);
|
||||
const rawValue = formattedToRawLookup.get(formattedValue);
|
||||
|
||||
return isDefined(rawValue) ? String(rawValue) : formattedValue;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return data.toSorted((a, b) =>
|
||||
getFormattedValue(a).localeCompare(getFormattedValue(b)),
|
||||
);
|
||||
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return data.toSorted((a, b) =>
|
||||
getFormattedValue(b).localeCompare(getFormattedValue(a)),
|
||||
);
|
||||
|
||||
case GraphOrderBy.FIELD_POSITION_ASC:
|
||||
case GraphOrderBy.FIELD_POSITION_DESC: {
|
||||
if (!isDefined(selectFieldOptions) || selectFieldOptions.length === 0) {
|
||||
throw new Error(
|
||||
'Select field options are required for field position sorting',
|
||||
);
|
||||
}
|
||||
|
||||
return sortBySelectOptionPosition({
|
||||
items: data,
|
||||
options: selectFieldOptions,
|
||||
formattedToRawLookup,
|
||||
getFormattedValue,
|
||||
direction: orderBy === GraphOrderBy.FIELD_POSITION_ASC ? 'ASC' : 'DESC',
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
assertUnreachable(orderBy);
|
||||
}
|
||||
};
|
||||
+13
@@ -1,6 +1,7 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsIn,
|
||||
@@ -71,6 +72,12 @@ export class BarChartConfigurationDTO
|
||||
@IsOptional()
|
||||
primaryAxisOrderBy?: GraphOrderBy;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
primaryAxisManualSortOrder?: string[];
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@@ -94,6 +101,12 @@ export class BarChartConfigurationDTO
|
||||
@IsOptional()
|
||||
secondaryAxisOrderBy?: GraphOrderBy;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
secondaryAxisManualSortOrder?: string[];
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
+13
@@ -1,6 +1,7 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsIn,
|
||||
@@ -72,6 +73,12 @@ export class LineChartConfigurationDTO
|
||||
@IsOptional()
|
||||
primaryAxisOrderBy?: GraphOrderBy;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
primaryAxisManualSortOrder?: string[];
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@@ -95,6 +102,12 @@ export class LineChartConfigurationDTO
|
||||
@IsOptional()
|
||||
secondaryAxisOrderBy?: GraphOrderBy;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
secondaryAxisManualSortOrder?: string[];
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
+7
@@ -1,6 +1,7 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsIn,
|
||||
@@ -70,6 +71,12 @@ export class PieChartConfigurationDTO
|
||||
@IsOptional()
|
||||
orderBy?: GraphOrderBy;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
manualSortOrder?: string[];
|
||||
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: false })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
+3
@@ -3,8 +3,11 @@ import { registerEnumType } from '@nestjs/graphql';
|
||||
export enum GraphOrderBy {
|
||||
FIELD_ASC = 'FIELD_ASC',
|
||||
FIELD_DESC = 'FIELD_DESC',
|
||||
FIELD_POSITION_ASC = 'FIELD_POSITION_ASC',
|
||||
FIELD_POSITION_DESC = 'FIELD_POSITION_DESC',
|
||||
VALUE_ASC = 'VALUE_ASC',
|
||||
VALUE_DESC = 'VALUE_DESC',
|
||||
MANUAL = 'MANUAL',
|
||||
}
|
||||
|
||||
registerEnumType(GraphOrderBy, {
|
||||
|
||||
@@ -46,57 +46,168 @@ const aggregateChartConfigSchema = z.object({
|
||||
});
|
||||
|
||||
// Graph configuration schema for BAR charts
|
||||
const barChartConfigSchema = z.object({
|
||||
graphType: z.literal(GraphType.BAR_CHART),
|
||||
aggregateFieldMetadataId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe('Field UUID to aggregate'),
|
||||
aggregateOperation: z.nativeEnum(AggregateOperations),
|
||||
primaryAxisGroupByFieldMetadataId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe('Field UUID to group by on primary axis'),
|
||||
secondaryAxisGroupByFieldMetadataId: z.string().uuid().optional(),
|
||||
primaryAxisOrderBy: z
|
||||
.enum(['FIELD_ASC', 'FIELD_DESC', 'VALUE_ASC', 'VALUE_DESC'])
|
||||
.optional(),
|
||||
displayDataLabel: z.boolean().optional().default(false),
|
||||
displayLegend: z.boolean().optional().default(true),
|
||||
layout: z
|
||||
.enum(['VERTICAL', 'HORIZONTAL'])
|
||||
.optional()
|
||||
.default('VERTICAL')
|
||||
.describe('Layout orientation for bar charts'),
|
||||
filter: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
const barChartConfigSchema = z
|
||||
.object({
|
||||
graphType: z.literal(GraphType.BAR_CHART),
|
||||
aggregateFieldMetadataId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe('Field UUID to aggregate'),
|
||||
aggregateOperation: z.nativeEnum(AggregateOperations),
|
||||
primaryAxisGroupByFieldMetadataId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe('Field UUID to group by on primary axis'),
|
||||
secondaryAxisGroupByFieldMetadataId: z.string().uuid().optional(),
|
||||
primaryAxisOrderBy: z
|
||||
.enum([
|
||||
'FIELD_ASC',
|
||||
'FIELD_DESC',
|
||||
'FIELD_POSITION_ASC',
|
||||
'FIELD_POSITION_DESC',
|
||||
'VALUE_ASC',
|
||||
'VALUE_DESC',
|
||||
'MANUAL',
|
||||
])
|
||||
.optional(),
|
||||
primaryAxisManualSortOrder: z.array(z.string()).optional(),
|
||||
secondaryAxisOrderBy: z
|
||||
.enum([
|
||||
'FIELD_ASC',
|
||||
'FIELD_DESC',
|
||||
'FIELD_POSITION_ASC',
|
||||
'FIELD_POSITION_DESC',
|
||||
'VALUE_ASC',
|
||||
'VALUE_DESC',
|
||||
'MANUAL',
|
||||
])
|
||||
.optional(),
|
||||
secondaryAxisManualSortOrder: z.array(z.string()).optional(),
|
||||
displayDataLabel: z.boolean().optional().default(false),
|
||||
displayLegend: z.boolean().optional().default(true),
|
||||
layout: z
|
||||
.enum(['VERTICAL', 'HORIZONTAL'])
|
||||
.optional()
|
||||
.default('VERTICAL')
|
||||
.describe('Layout orientation for bar charts'),
|
||||
filter: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.primaryAxisOrderBy !== 'MANUAL' ||
|
||||
(Array.isArray(data.primaryAxisManualSortOrder) &&
|
||||
data.primaryAxisManualSortOrder.length > 0),
|
||||
{
|
||||
message:
|
||||
'primaryAxisManualSortOrder must be a non-empty array when primaryAxisOrderBy is MANUAL',
|
||||
path: ['primaryAxisManualSortOrder'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
data.secondaryAxisOrderBy !== 'MANUAL' ||
|
||||
(Array.isArray(data.secondaryAxisManualSortOrder) &&
|
||||
data.secondaryAxisManualSortOrder.length > 0),
|
||||
{
|
||||
message:
|
||||
'secondaryAxisManualSortOrder must be a non-empty array when secondaryAxisOrderBy is MANUAL',
|
||||
path: ['secondaryAxisManualSortOrder'],
|
||||
},
|
||||
);
|
||||
|
||||
// Graph configuration schema for LINE charts
|
||||
const lineChartConfigSchema = z.object({
|
||||
graphType: z.literal(GraphType.LINE_CHART),
|
||||
aggregateFieldMetadataId: z.string().uuid(),
|
||||
aggregateOperation: z.nativeEnum(AggregateOperations),
|
||||
primaryAxisGroupByFieldMetadataId: z.string().uuid(),
|
||||
secondaryAxisGroupByFieldMetadataId: z.string().uuid().optional(),
|
||||
primaryAxisOrderBy: z
|
||||
.enum(['FIELD_ASC', 'FIELD_DESC', 'VALUE_ASC', 'VALUE_DESC'])
|
||||
.optional(),
|
||||
displayDataLabel: z.boolean().optional().default(false),
|
||||
filter: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
const lineChartConfigSchema = z
|
||||
.object({
|
||||
graphType: z.literal(GraphType.LINE_CHART),
|
||||
aggregateFieldMetadataId: z.string().uuid(),
|
||||
aggregateOperation: z.nativeEnum(AggregateOperations),
|
||||
primaryAxisGroupByFieldMetadataId: z.string().uuid(),
|
||||
secondaryAxisGroupByFieldMetadataId: z.string().uuid().optional(),
|
||||
primaryAxisOrderBy: z
|
||||
.enum([
|
||||
'FIELD_ASC',
|
||||
'FIELD_DESC',
|
||||
'FIELD_POSITION_ASC',
|
||||
'FIELD_POSITION_DESC',
|
||||
'VALUE_ASC',
|
||||
'VALUE_DESC',
|
||||
'MANUAL',
|
||||
])
|
||||
.optional(),
|
||||
primaryAxisManualSortOrder: z.array(z.string()).optional(),
|
||||
secondaryAxisOrderBy: z
|
||||
.enum([
|
||||
'FIELD_ASC',
|
||||
'FIELD_DESC',
|
||||
'FIELD_POSITION_ASC',
|
||||
'FIELD_POSITION_DESC',
|
||||
'VALUE_ASC',
|
||||
'VALUE_DESC',
|
||||
'MANUAL',
|
||||
])
|
||||
.optional(),
|
||||
secondaryAxisManualSortOrder: z.array(z.string()).optional(),
|
||||
displayDataLabel: z.boolean().optional().default(false),
|
||||
filter: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.primaryAxisOrderBy !== 'MANUAL' ||
|
||||
(Array.isArray(data.primaryAxisManualSortOrder) &&
|
||||
data.primaryAxisManualSortOrder.length > 0),
|
||||
{
|
||||
message:
|
||||
'primaryAxisManualSortOrder must be a non-empty array when primaryAxisOrderBy is MANUAL',
|
||||
path: ['primaryAxisManualSortOrder'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
data.secondaryAxisOrderBy !== 'MANUAL' ||
|
||||
(Array.isArray(data.secondaryAxisManualSortOrder) &&
|
||||
data.secondaryAxisManualSortOrder.length > 0),
|
||||
{
|
||||
message:
|
||||
'secondaryAxisManualSortOrder must be a non-empty array when secondaryAxisOrderBy is MANUAL',
|
||||
path: ['secondaryAxisManualSortOrder'],
|
||||
},
|
||||
);
|
||||
|
||||
// Graph configuration schema for PIE charts
|
||||
const pieChartConfigSchema = z.object({
|
||||
graphType: z.literal(GraphType.PIE_CHART),
|
||||
aggregateFieldMetadataId: z.string().uuid(),
|
||||
aggregateOperation: z.nativeEnum(AggregateOperations),
|
||||
groupByFieldMetadataId: z.string().uuid().describe('Field UUID to slice by'),
|
||||
orderBy: z
|
||||
.enum(['FIELD_ASC', 'FIELD_DESC', 'VALUE_ASC', 'VALUE_DESC'])
|
||||
.optional(),
|
||||
displayDataLabel: z.boolean().optional().default(true),
|
||||
filter: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
const pieChartConfigSchema = z
|
||||
.object({
|
||||
graphType: z.literal(GraphType.PIE_CHART),
|
||||
aggregateFieldMetadataId: z.string().uuid(),
|
||||
aggregateOperation: z.nativeEnum(AggregateOperations),
|
||||
groupByFieldMetadataId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe('Field UUID to slice by'),
|
||||
orderBy: z
|
||||
.enum([
|
||||
'FIELD_ASC',
|
||||
'FIELD_DESC',
|
||||
'FIELD_POSITION_ASC',
|
||||
'FIELD_POSITION_DESC',
|
||||
'VALUE_ASC',
|
||||
'VALUE_DESC',
|
||||
'MANUAL',
|
||||
])
|
||||
.optional(),
|
||||
manualSortOrder: z.array(z.string()).optional(),
|
||||
displayDataLabel: z.boolean().optional().default(true),
|
||||
filter: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.orderBy !== 'MANUAL' ||
|
||||
(Array.isArray(data.manualSortOrder) && data.manualSortOrder.length > 0),
|
||||
{
|
||||
message:
|
||||
'manualSortOrder must be a non-empty array when orderBy is MANUAL',
|
||||
path: ['manualSortOrder'],
|
||||
},
|
||||
);
|
||||
|
||||
// Iframe configuration
|
||||
const iframeConfigSchema = z.object({
|
||||
|
||||
+5
@@ -7,10 +7,12 @@ export const WIDGET_CONFIGURATION_GQL_FIELDS = `
|
||||
primaryAxisGroupBySubFieldName
|
||||
primaryAxisDateGranularity
|
||||
primaryAxisOrderBy
|
||||
primaryAxisManualSortOrder
|
||||
secondaryAxisGroupByFieldMetadataId
|
||||
secondaryAxisGroupBySubFieldName
|
||||
secondaryAxisGroupByDateGranularity
|
||||
secondaryAxisOrderBy
|
||||
secondaryAxisManualSortOrder
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
@@ -34,10 +36,12 @@ export const WIDGET_CONFIGURATION_GQL_FIELDS = `
|
||||
primaryAxisGroupBySubFieldName
|
||||
primaryAxisDateGranularity
|
||||
primaryAxisOrderBy
|
||||
primaryAxisManualSortOrder
|
||||
secondaryAxisGroupByFieldMetadataId
|
||||
secondaryAxisGroupBySubFieldName
|
||||
secondaryAxisGroupByDateGranularity
|
||||
secondaryAxisOrderBy
|
||||
secondaryAxisManualSortOrder
|
||||
omitNullValues
|
||||
axisNameDisplay
|
||||
displayDataLabel
|
||||
@@ -60,6 +64,7 @@ export const WIDGET_CONFIGURATION_GQL_FIELDS = `
|
||||
groupBySubFieldName
|
||||
dateGranularity
|
||||
orderBy
|
||||
manualSortOrder
|
||||
displayDataLabel
|
||||
showCenterMetric
|
||||
displayLegend
|
||||
|
||||
+2
@@ -31,6 +31,7 @@ exports[`Page layout with tabs update should succeed should update page layout w
|
||||
"firstDayOfTheWeek": 1,
|
||||
"groupByFieldMetadataId": Any<String>,
|
||||
"groupBySubFieldName": null,
|
||||
"manualSortOrder": null,
|
||||
"orderBy": "VALUE_DESC",
|
||||
"showCenterMetric": true,
|
||||
"timezone": "UTC",
|
||||
@@ -120,6 +121,7 @@ exports[`Page layout with tabs update should succeed should update page layout w
|
||||
"firstDayOfTheWeek": 1,
|
||||
"groupByFieldMetadataId": Any<String>,
|
||||
"groupBySubFieldName": null,
|
||||
"manualSortOrder": null,
|
||||
"orderBy": "VALUE_DESC",
|
||||
"showCenterMetric": true,
|
||||
"timezone": "UTC",
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
import { isFieldMetadataNumericKind } from '../isFieldMetadataNumericKind';
|
||||
|
||||
describe('isFieldMetadataNumericKind', () => {
|
||||
it.each([
|
||||
FieldMetadataType.NUMBER,
|
||||
FieldMetadataType.NUMERIC,
|
||||
FieldMetadataType.CURRENCY,
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.POSITION,
|
||||
])('should return true for %s', (type) => {
|
||||
expect(isFieldMetadataNumericKind(type)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([FieldMetadataType.TEXT, FieldMetadataType.SELECT])(
|
||||
'should return false for %s',
|
||||
(type) => {
|
||||
expect(isFieldMetadataNumericKind(type)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
import { isFieldMetadataSelectKind } from '../isFieldMetadataSelectKind';
|
||||
|
||||
describe('isFieldMetadataSelectKind', () => {
|
||||
it.each([FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT])(
|
||||
'should return true for %s',
|
||||
(type) => {
|
||||
expect(isFieldMetadataSelectKind(type)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([FieldMetadataType.TEXT, FieldMetadataType.NUMBER])(
|
||||
'should return false for %s',
|
||||
(type) => {
|
||||
expect(isFieldMetadataSelectKind(type)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
import { isFieldMetadataTextKind } from '../isFieldMetadataTextKind';
|
||||
|
||||
describe('isFieldMetadataTextKind', () => {
|
||||
it.each([
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
])('should return true for %s', (type) => {
|
||||
expect(isFieldMetadataTextKind(type)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([FieldMetadataType.NUMBER, FieldMetadataType.SELECT])(
|
||||
'should return false for %s',
|
||||
(type) => {
|
||||
expect(isFieldMetadataTextKind(type)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1 +1,5 @@
|
||||
export * from './isFieldMetadataDateKind';
|
||||
export * from './isFieldMetadataNumericKind';
|
||||
export * from './isFieldMetadataSelectKind';
|
||||
export * from './isFieldMetadataTextKind';
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
|
||||
const NUMBER_FIELD_TYPES: FieldMetadataType[] = [
|
||||
FieldMetadataType.NUMBER,
|
||||
FieldMetadataType.NUMERIC,
|
||||
FieldMetadataType.CURRENCY,
|
||||
FieldMetadataType.RATING,
|
||||
FieldMetadataType.POSITION,
|
||||
];
|
||||
|
||||
export const isFieldMetadataNumericKind = (
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
): boolean => {
|
||||
return NUMBER_FIELD_TYPES.includes(fieldMetadataType);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
|
||||
export const isFieldMetadataSelectKind = (
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
): boolean => {
|
||||
return (
|
||||
fieldMetadataType === FieldMetadataType.SELECT ||
|
||||
fieldMetadataType === FieldMetadataType.MULTI_SELECT
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
|
||||
const TEXT_FIELD_TYPES: FieldMetadataType[] = [
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.RICH_TEXT,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
];
|
||||
|
||||
export const isFieldMetadataTextKind = (
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
): boolean => {
|
||||
return TEXT_FIELD_TYPES.includes(fieldMetadataType);
|
||||
};
|
||||
@@ -37,6 +37,9 @@ export { evalFromContext } from './evalFromContext';
|
||||
export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';
|
||||
export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name';
|
||||
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
|
||||
export { isFieldMetadataNumericKind } from './fieldMetadata/isFieldMetadataNumericKind';
|
||||
export { isFieldMetadataSelectKind } from './fieldMetadata/isFieldMetadataSelectKind';
|
||||
export { isFieldMetadataTextKind } from './fieldMetadata/isFieldMetadataTextKind';
|
||||
export { checkIfShouldComputeEmptinessFilter } from './filter/checkIfShouldComputeEmptinessFilter';
|
||||
export { checkIfShouldSkipFiltering } from './filter/checkIfShouldSkipFiltering';
|
||||
export { computeGqlOperationFilterForEmails } from './filter/compute-record-gql-operation-filter/for-composite-field/computeGqlOperationFilterForEmails';
|
||||
|
||||
@@ -319,8 +319,13 @@ export {
|
||||
IconShield,
|
||||
IconSitemap,
|
||||
IconSlash,
|
||||
IconSortAscending,
|
||||
IconSortAscendingLetters,
|
||||
IconSortAscendingNumbers,
|
||||
IconSortAZ,
|
||||
IconSortDescending,
|
||||
IconSortDescendingLetters,
|
||||
IconSortDescendingNumbers,
|
||||
IconSortZA,
|
||||
IconSparkles,
|
||||
IconSpy,
|
||||
|
||||
@@ -385,8 +385,13 @@ export {
|
||||
IconShield,
|
||||
IconSitemap,
|
||||
IconSlash,
|
||||
IconSortAscending,
|
||||
IconSortAscendingLetters,
|
||||
IconSortAscendingNumbers,
|
||||
IconSortAZ,
|
||||
IconSortDescending,
|
||||
IconSortDescendingLetters,
|
||||
IconSortDescendingNumbers,
|
||||
IconSortZA,
|
||||
IconSparkles,
|
||||
IconSpy,
|
||||
|
||||
Reference in New Issue
Block a user