part 3 of on click bar to filters: add sort plus some normalizations (#16075)
This commit is contained in:
+6
-1
@@ -54,7 +54,12 @@ export const ChartColorSelectionDropdownContent = () => {
|
||||
|
||||
const configuration = widgetInEditMode.configuration as ChartConfiguration;
|
||||
|
||||
if (!('color' in configuration)) {
|
||||
if (
|
||||
configuration.__typename !== 'BarChartConfiguration' &&
|
||||
configuration.__typename !== 'LineChartConfiguration' &&
|
||||
configuration.__typename !== 'GaugeChartConfiguration' &&
|
||||
configuration.__typename !== 'PieChartConfiguration'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+15
-10
@@ -44,6 +44,13 @@ export const useChartSettingsValues = ({
|
||||
configuration.__typename === 'BarChartConfiguration' ||
|
||||
configuration.__typename === 'LineChartConfiguration';
|
||||
|
||||
const hasColorProperty =
|
||||
isBarOrLineChart ||
|
||||
configuration.__typename === 'GaugeChartConfiguration' ||
|
||||
configuration.__typename === 'PieChartConfiguration';
|
||||
|
||||
const isPieChart = configuration.__typename === 'PieChartConfiguration';
|
||||
|
||||
let groupByFieldXId: string | undefined;
|
||||
let groupByFieldYId: string | undefined;
|
||||
let groupBySubFieldNameX: CompositeFieldSubFieldName | undefined;
|
||||
@@ -147,7 +154,7 @@ export const useChartSettingsValues = ({
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_X:
|
||||
return groupBySubFieldNameXLabel ?? groupByFieldX?.label;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.COLORS:
|
||||
return 'color' in configuration && isDefined(configuration.color)
|
||||
return hasColorProperty && isDefined(configuration.color)
|
||||
? capitalize(configuration.color)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y:
|
||||
@@ -183,8 +190,7 @@ export const useChartSettingsValues = ({
|
||||
case CHART_CONFIGURATION_SETTING_IDS.GROUP_BY:
|
||||
return groupByFieldY?.label;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.AXIS_NAME:
|
||||
return 'axisNameDisplay' in configuration &&
|
||||
isDefined(configuration.axisNameDisplay)
|
||||
return isBarOrLineChart && isDefined(configuration.axisNameDisplay)
|
||||
? getChartAxisNameDisplayOptions(configuration.axisNameDisplay)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.PRIMARY_SORT_BY:
|
||||
@@ -202,32 +208,31 @@ export const useChartSettingsValues = ({
|
||||
? configuration.isStacked !== false
|
||||
: true;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.OMIT_NULL_VALUES:
|
||||
return 'omitNullValues' in configuration
|
||||
return isBarOrLineChart
|
||||
? (configuration.omitNullValues ?? false)
|
||||
: false;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE:
|
||||
return 'rangeMin' in configuration
|
||||
return isBarOrLineChart
|
||||
? (configuration.rangeMin?.toString() ?? '')
|
||||
: '';
|
||||
case CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE:
|
||||
return 'rangeMax' in configuration
|
||||
return isBarOrLineChart
|
||||
? (configuration.rangeMax?.toString() ?? '')
|
||||
: '';
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_X:
|
||||
return 'primaryAxisDateGranularity' in configuration &&
|
||||
return isBarOrLineChart &&
|
||||
isDefined(configuration.primaryAxisDateGranularity)
|
||||
? getDateGranularityLabel(configuration.primaryAxisDateGranularity)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY_Y:
|
||||
return 'secondaryAxisGroupByDateGranularity' in configuration &&
|
||||
return isBarOrLineChart &&
|
||||
isDefined(configuration.secondaryAxisGroupByDateGranularity)
|
||||
? getDateGranularityLabel(
|
||||
configuration.secondaryAxisGroupByDateGranularity,
|
||||
)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATE_GRANULARITY:
|
||||
return 'dateGranularity' in configuration &&
|
||||
isDefined(configuration.dateGranularity)
|
||||
return isPieChart && isDefined(configuration.dateGranularity)
|
||||
? getDateGranularityLabel(configuration.dateGranularity)
|
||||
: undefined;
|
||||
default:
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import {
|
||||
type GraphOrderBy,
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type BarLineChartConvertibleFields = {
|
||||
primaryAxisGroupByFieldMetadataId?: string;
|
||||
primaryAxisGroupBySubFieldName?: string | null;
|
||||
primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null;
|
||||
primaryAxisOrderBy?: GraphOrderBy | null;
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import {
|
||||
type GraphOrderBy,
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type PieChartConvertibleFields = {
|
||||
groupByFieldMetadataId?: string;
|
||||
groupBySubFieldName?: string | null;
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity | null;
|
||||
orderBy?: GraphOrderBy | null;
|
||||
};
|
||||
+13
-20
@@ -1,3 +1,4 @@
|
||||
import { type PieChartConvertibleFields } from '@/command-menu/pages/page-layout/types/PieChartConvertibleFields';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type LineChartConfiguration,
|
||||
@@ -5,26 +6,18 @@ import {
|
||||
|
||||
export const convertBarOrLineChartConfigToPieChart = (
|
||||
configuration: BarChartConfiguration | LineChartConfiguration,
|
||||
): Record<string, any> => {
|
||||
const configToUpdate: Record<string, any> = {};
|
||||
|
||||
if ('primaryAxisGroupByFieldMetadataId' in configuration) {
|
||||
configToUpdate.groupByFieldMetadataId =
|
||||
configuration.primaryAxisGroupByFieldMetadataId;
|
||||
): PieChartConvertibleFields => {
|
||||
if (
|
||||
configuration.__typename !== 'BarChartConfiguration' &&
|
||||
configuration.__typename !== 'LineChartConfiguration'
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if ('primaryAxisGroupBySubFieldName' in configuration) {
|
||||
configToUpdate.groupBySubFieldName =
|
||||
configuration.primaryAxisGroupBySubFieldName;
|
||||
}
|
||||
|
||||
if ('primaryAxisDateGranularity' in configuration) {
|
||||
configToUpdate.dateGranularity = configuration.primaryAxisDateGranularity;
|
||||
}
|
||||
|
||||
if ('primaryAxisOrderBy' in configuration) {
|
||||
configToUpdate.orderBy = configuration.primaryAxisOrderBy;
|
||||
}
|
||||
|
||||
return configToUpdate;
|
||||
return {
|
||||
groupByFieldMetadataId: configuration.primaryAxisGroupByFieldMetadataId,
|
||||
groupBySubFieldName: configuration.primaryAxisGroupBySubFieldName,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity,
|
||||
orderBy: configuration.primaryAxisOrderBy,
|
||||
};
|
||||
};
|
||||
|
||||
+10
-20
@@ -1,27 +1,17 @@
|
||||
import { type BarLineChartConvertibleFields } from '@/command-menu/pages/page-layout/types/BarLineChartConvertibleFields';
|
||||
import { type PieChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
export const convertPieChartConfigToBarOrLineChart = (
|
||||
configuration: PieChartConfiguration,
|
||||
): Record<string, any> => {
|
||||
const configToUpdate: Record<string, any> = {};
|
||||
|
||||
if ('groupByFieldMetadataId' in configuration) {
|
||||
configToUpdate.primaryAxisGroupByFieldMetadataId =
|
||||
configuration.groupByFieldMetadataId;
|
||||
): BarLineChartConvertibleFields => {
|
||||
if (configuration.__typename !== 'PieChartConfiguration') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if ('groupBySubFieldName' in configuration) {
|
||||
configToUpdate.primaryAxisGroupBySubFieldName =
|
||||
configuration.groupBySubFieldName;
|
||||
}
|
||||
|
||||
if ('dateGranularity' in configuration) {
|
||||
configToUpdate.primaryAxisDateGranularity = configuration.dateGranularity;
|
||||
}
|
||||
|
||||
if ('orderBy' in configuration) {
|
||||
configToUpdate.primaryAxisOrderBy = configuration.orderBy;
|
||||
}
|
||||
|
||||
return configToUpdate;
|
||||
return {
|
||||
primaryAxisGroupByFieldMetadataId: configuration.groupByFieldMetadataId,
|
||||
primaryAxisGroupBySubFieldName: configuration.groupBySubFieldName,
|
||||
primaryAxisDateGranularity: configuration.dateGranularity,
|
||||
primaryAxisOrderBy: configuration.orderBy,
|
||||
};
|
||||
};
|
||||
|
||||
+8
-2
@@ -9,9 +9,16 @@ export const isMinMaxRangeValid = (
|
||||
newValue: number,
|
||||
configuration: ChartConfiguration,
|
||||
): boolean => {
|
||||
const isBarOrLineChart =
|
||||
configuration.__typename === 'BarChartConfiguration' ||
|
||||
configuration.__typename === 'LineChartConfiguration';
|
||||
|
||||
if (!isBarOrLineChart) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (settingId === CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE) {
|
||||
if (
|
||||
'rangeMax' in configuration &&
|
||||
isDefined(configuration.rangeMax) &&
|
||||
newValue > configuration.rangeMax
|
||||
) {
|
||||
@@ -21,7 +28,6 @@ export const isMinMaxRangeValid = (
|
||||
|
||||
if (settingId === CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE) {
|
||||
if (
|
||||
'rangeMin' in configuration &&
|
||||
isDefined(configuration.rangeMin) &&
|
||||
newValue < configuration.rangeMin
|
||||
) {
|
||||
|
||||
+25
-10
@@ -2,12 +2,13 @@ import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/Char
|
||||
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
|
||||
import { useGraphPieChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/useGraphPieChartWidgetData';
|
||||
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
|
||||
import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams';
|
||||
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
type PageLayoutWidget,
|
||||
type PieChartConfiguration,
|
||||
@@ -31,6 +32,7 @@ export const GraphWidgetPieChartRenderer = ({
|
||||
loading,
|
||||
hasTooManyGroups,
|
||||
objectMetadataItem,
|
||||
formattedToRawLookup,
|
||||
showDataLabels,
|
||||
} = useGraphPieChartWidgetData({
|
||||
objectMetadataItemId: widget.objectMetadataId,
|
||||
@@ -38,6 +40,7 @@ export const GraphWidgetPieChartRenderer = ({
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const configuration = widget.configuration as PieChartConfiguration;
|
||||
|
||||
const indexViewId = useRecoilValue(
|
||||
coreIndexViewIdFromObjectMetadataItemFamilySelector({
|
||||
@@ -45,16 +48,28 @@ export const GraphWidgetPieChartRenderer = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const handleSliceClick = (_datum: PieChartDataItem) => {
|
||||
return navigate(
|
||||
getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
},
|
||||
isDefined(indexViewId) ? { viewId: indexViewId } : undefined,
|
||||
),
|
||||
const handleSliceClick = (datum: PieChartDataItem) => {
|
||||
const rawValue = formattedToRawLookup.get(datum.id) ?? null;
|
||||
|
||||
const drilldownQueryParams = buildChartDrilldownQueryParams({
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
clickedData: {
|
||||
primaryBucketRawValue: rawValue,
|
||||
},
|
||||
viewId: indexViewId,
|
||||
timezone: configuration.timezone ?? undefined,
|
||||
});
|
||||
|
||||
const url = getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
},
|
||||
Object.fromEntries(drilldownQueryParams),
|
||||
);
|
||||
|
||||
return navigate(url);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/
|
||||
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
|
||||
import { transformGroupByDataToPieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData';
|
||||
import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { useMemo } from 'react';
|
||||
import { type PieChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
@@ -18,6 +19,7 @@ type UseGraphPieChartWidgetDataResult = {
|
||||
error?: Error;
|
||||
hasTooManyGroups: boolean;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
showDataLabels: boolean;
|
||||
};
|
||||
|
||||
|
||||
+17
-11
@@ -6,8 +6,10 @@ import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/
|
||||
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
|
||||
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 { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type PieChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
@@ -21,11 +23,13 @@ type TransformGroupByDataToPieChartDataParams = {
|
||||
type TransformGroupByDataToPieChartDataResult = {
|
||||
data: PieChartDataItem[];
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
const EMPTY_PIE_CHART_RESULT: TransformGroupByDataToPieChartDataResult = {
|
||||
data: [],
|
||||
hasTooManyGroups: false,
|
||||
formattedToRawLookup: new Map(),
|
||||
};
|
||||
|
||||
export const transformGroupByDataToPieChartData = ({
|
||||
@@ -65,17 +69,18 @@ export const transformGroupByDataToPieChartData = ({
|
||||
PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
|
||||
);
|
||||
|
||||
const data: PieChartDataItem[] = limitedResults.map((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: limitedResults,
|
||||
primaryAxisGroupByField: groupByField,
|
||||
primaryAxisDateGranularity: configuration.dateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName:
|
||||
configuration.groupBySubFieldName ?? undefined,
|
||||
});
|
||||
|
||||
const label = isDefined(dimensionValues?.[0])
|
||||
? formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByField,
|
||||
dateGranularity: configuration.dateGranularity ?? undefined,
|
||||
subFieldName: configuration.groupBySubFieldName ?? undefined,
|
||||
})
|
||||
: '';
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
|
||||
const data: PieChartDataItem[] = limitedResults.map((result, index) => {
|
||||
const label = formattedValues[index]?.formattedPrimaryDimensionValue ?? '';
|
||||
|
||||
const value = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
@@ -97,5 +102,6 @@ export const transformGroupByDataToPieChartData = ({
|
||||
return {
|
||||
data,
|
||||
hasTooManyGroups: rawResults.length > PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
|
||||
+5
-1
@@ -3,11 +3,15 @@ import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDim
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type LineChartConfiguration,
|
||||
type PieChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type BuildChartDrilldownQueryParamsInput = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
configuration: BarChartConfiguration | LineChartConfiguration;
|
||||
configuration:
|
||||
| BarChartConfiguration
|
||||
| LineChartConfiguration
|
||||
| PieChartConfiguration;
|
||||
clickedData: {
|
||||
primaryBucketRawValue: RawDimensionValue;
|
||||
};
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { buildSortsForChartFieldOrderBy } from '@/page-layout/widgets/graph/utils/buildSortsForChartFieldOrderBy';
|
||||
import { type NormalizedChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
describe('buildSortsForChartFieldOrderBy', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [
|
||||
{
|
||||
id: 'field-status',
|
||||
name: 'status',
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Status',
|
||||
},
|
||||
{
|
||||
id: 'field-createdAt',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Created At',
|
||||
},
|
||||
{
|
||||
id: 'field-address',
|
||||
name: 'address',
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
label: 'Address',
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
it('should return ASC sort for FIELD_ASC orderBy', () => {
|
||||
const normalizedFields: NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId: 'field-createdAt',
|
||||
groupBySubFieldName: undefined,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
};
|
||||
|
||||
const result = buildSortsForChartFieldOrderBy({
|
||||
normalizedFields,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ fieldName: 'createdAt', direction: 'ASC' });
|
||||
});
|
||||
|
||||
it('should return DESC sort for FIELD_DESC orderBy', () => {
|
||||
const normalizedFields: NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId: 'field-status',
|
||||
groupBySubFieldName: undefined,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
};
|
||||
|
||||
const result = buildSortsForChartFieldOrderBy({
|
||||
normalizedFields,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ fieldName: 'status', direction: 'DESC' });
|
||||
});
|
||||
|
||||
it('should include subfield for composite fields', () => {
|
||||
const normalizedFields: NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId: 'field-address',
|
||||
groupBySubFieldName: 'addressCity',
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
};
|
||||
|
||||
const result = buildSortsForChartFieldOrderBy({
|
||||
normalizedFields,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
fieldName: 'address.addressCity',
|
||||
direction: 'ASC',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle composite field without subfield', () => {
|
||||
const normalizedFields: NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId: 'field-address',
|
||||
groupBySubFieldName: null,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
};
|
||||
|
||||
const result = buildSortsForChartFieldOrderBy({
|
||||
normalizedFields,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ fieldName: 'address', direction: 'ASC' });
|
||||
});
|
||||
|
||||
it('should return null when groupBy field not found', () => {
|
||||
const normalizedFields: NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId: 'non-existent',
|
||||
groupBySubFieldName: undefined,
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
};
|
||||
|
||||
const result = buildSortsForChartFieldOrderBy({
|
||||
normalizedFields,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { buildSortsForChartValueOrderBy } from '@/page-layout/widgets/graph/utils/buildSortsForChartValueOrderBy';
|
||||
import { type NormalizedChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
describe('buildSortsForChartValueOrderBy', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [
|
||||
{
|
||||
id: 'field-amount',
|
||||
name: 'amount',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Amount',
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
it('should return ASC sort for VALUE_ASC orderBy', () => {
|
||||
const normalizedFields: NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId: 'some-field',
|
||||
groupBySubFieldName: undefined,
|
||||
orderBy: GraphOrderBy.VALUE_ASC,
|
||||
};
|
||||
|
||||
const result = buildSortsForChartValueOrderBy({
|
||||
normalizedFields,
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ fieldName: 'amount', direction: 'ASC' });
|
||||
});
|
||||
|
||||
it('should return DESC sort for VALUE_DESC orderBy', () => {
|
||||
const normalizedFields: NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId: 'some-field',
|
||||
groupBySubFieldName: undefined,
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
};
|
||||
|
||||
const result = buildSortsForChartValueOrderBy({
|
||||
normalizedFields,
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ fieldName: 'amount', direction: 'DESC' });
|
||||
});
|
||||
|
||||
it('should return null when aggregate field not found', () => {
|
||||
const normalizedFields: NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId: 'some-field',
|
||||
groupBySubFieldName: undefined,
|
||||
orderBy: GraphOrderBy.VALUE_ASC,
|
||||
};
|
||||
|
||||
const result = buildSortsForChartValueOrderBy({
|
||||
normalizedFields,
|
||||
aggregateFieldMetadataId: 'non-existent',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { buildSortsFromChartConfig } from '@/page-layout/widgets/graph/utils/buildSortsFromChartConfig';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type PieChartConfiguration,
|
||||
GraphOrderBy,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
describe('buildSortsFromChartConfig', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [
|
||||
{
|
||||
id: 'field-status',
|
||||
name: 'status',
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Status',
|
||||
},
|
||||
{
|
||||
id: 'field-amount',
|
||||
name: 'amount',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Amount',
|
||||
},
|
||||
{
|
||||
id: 'field-createdAt',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Created At',
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
describe('integration', () => {
|
||||
it('should handle field-based sorting end-to-end for Bar charts', () => {
|
||||
const config = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-createdAt',
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
} as BarChartConfiguration;
|
||||
|
||||
const result = buildSortsFromChartConfig({
|
||||
configuration: config,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ fieldName: 'createdAt', direction: 'ASC' }]);
|
||||
});
|
||||
|
||||
it('should handle value-based sorting end-to-end for Pie charts', () => {
|
||||
const config = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
orderBy: GraphOrderBy.VALUE_DESC,
|
||||
groupByFieldMetadataId: 'field-status',
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
} as PieChartConfiguration;
|
||||
|
||||
const result = buildSortsFromChartConfig({
|
||||
configuration: config,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual([{ fieldName: 'amount', direction: 'DESC' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return empty array when orderBy is undefined', () => {
|
||||
const config = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisOrderBy: undefined,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-createdAt',
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
} as BarChartConfiguration;
|
||||
|
||||
const result = buildSortsFromChartConfig({
|
||||
configuration: config,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when orderBy is null', () => {
|
||||
const config = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
orderBy: null,
|
||||
groupByFieldMetadataId: 'field-status',
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
} as PieChartConfiguration;
|
||||
|
||||
const result = buildSortsFromChartConfig({
|
||||
configuration: config,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when field not found', () => {
|
||||
const config = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
primaryAxisGroupByFieldMetadataId: 'non-existent',
|
||||
aggregateFieldMetadataId: 'field-amount',
|
||||
} as BarChartConfiguration;
|
||||
|
||||
const result = buildSortsFromChartConfig({
|
||||
configuration: config,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
AggregateOperations,
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { normalizeChartConfigurationFields } from '../normalizeChartConfigurationFields';
|
||||
|
||||
describe('normalizeChartConfigurationFields', () => {
|
||||
describe('Bar and Line charts (with primaryAxis prefix)', () => {
|
||||
it('should extract fields from Bar chart configuration', () => {
|
||||
const barConfig = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'field-123',
|
||||
primaryAxisGroupBySubFieldName: 'subField',
|
||||
primaryAxisDateGranularity: ObjectRecordGroupByDateGranularity.MONTH,
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
aggregateFieldMetadataId: 'aggregate-456',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
} as any;
|
||||
|
||||
const result = normalizeChartConfigurationFields(barConfig);
|
||||
|
||||
expect(result.groupByFieldMetadataId).toBe('field-123');
|
||||
expect(result.groupBySubFieldName).toBe('subField');
|
||||
expect(result.dateGranularity).toBe(
|
||||
ObjectRecordGroupByDateGranularity.MONTH,
|
||||
);
|
||||
expect(result.orderBy).toBe(GraphOrderBy.FIELD_ASC);
|
||||
});
|
||||
|
||||
it('should extract fields from Line chart configuration', () => {
|
||||
const lineConfig = {
|
||||
__typename: 'LineChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'field-789',
|
||||
primaryAxisOrderBy: GraphOrderBy.VALUE_DESC,
|
||||
aggregateFieldMetadataId: 'aggregate-012',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
graphType: GraphType.LINE,
|
||||
} as any;
|
||||
|
||||
const result = normalizeChartConfigurationFields(lineConfig);
|
||||
|
||||
expect(result.groupByFieldMetadataId).toBe('field-789');
|
||||
expect(result.orderBy).toBe(GraphOrderBy.VALUE_DESC);
|
||||
expect(result.groupBySubFieldName).toBeUndefined();
|
||||
expect(result.dateGranularity).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pie charts (without prefix)', () => {
|
||||
it('should extract fields from Pie chart configuration', () => {
|
||||
const pieConfig = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
groupByFieldMetadataId: 'pie-field',
|
||||
groupBySubFieldName: 'pieSubField',
|
||||
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
aggregateFieldMetadataId: 'pie-aggregate',
|
||||
aggregateOperation: AggregateOperations.AVG,
|
||||
graphType: GraphType.PIE,
|
||||
} as any;
|
||||
|
||||
const result = normalizeChartConfigurationFields(pieConfig);
|
||||
|
||||
expect(result.groupByFieldMetadataId).toBe('pie-field');
|
||||
expect(result.groupBySubFieldName).toBe('pieSubField');
|
||||
expect(result.dateGranularity).toBe(
|
||||
ObjectRecordGroupByDateGranularity.DAY,
|
||||
);
|
||||
expect(result.orderBy).toBe(GraphOrderBy.FIELD_DESC);
|
||||
});
|
||||
|
||||
it('should handle minimal Pie chart configuration', () => {
|
||||
const pieConfig = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
groupByFieldMetadataId: 'minimal-field',
|
||||
aggregateFieldMetadataId: 'minimal-aggregate',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
graphType: GraphType.PIE,
|
||||
} as any;
|
||||
|
||||
const result = normalizeChartConfigurationFields(pieConfig);
|
||||
|
||||
expect(result.groupByFieldMetadataId).toBe('minimal-field');
|
||||
expect(result.groupBySubFieldName).toBeUndefined();
|
||||
expect(result.dateGranularity).toBeUndefined();
|
||||
expect(result.orderBy).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should return empty object for configuration without recognized fields', () => {
|
||||
const unknownConfig = {
|
||||
someOtherField: 'value',
|
||||
aggregateFieldMetadataId: 'ignored',
|
||||
} as any;
|
||||
|
||||
const result = normalizeChartConfigurationFields(unknownConfig);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should use __typename to determine fields even when both patterns exist', () => {
|
||||
const mixedConfig = {
|
||||
__typename: 'BarChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'primary-field',
|
||||
groupByFieldMetadataId: 'fallback-field',
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
} as any;
|
||||
|
||||
const result = normalizeChartConfigurationFields(mixedConfig);
|
||||
|
||||
expect(result.groupByFieldMetadataId).toBe('primary-field');
|
||||
expect(result.orderBy).toBe(GraphOrderBy.FIELD_ASC);
|
||||
});
|
||||
|
||||
it('should extract Pie fields when __typename is PieChartConfiguration even if primaryAxis fields exist', () => {
|
||||
const mixedConfig = {
|
||||
__typename: 'PieChartConfiguration',
|
||||
primaryAxisGroupByFieldMetadataId: 'primary-field',
|
||||
groupByFieldMetadataId: 'pie-field',
|
||||
primaryAxisOrderBy: GraphOrderBy.FIELD_ASC,
|
||||
orderBy: GraphOrderBy.FIELD_DESC,
|
||||
} as any;
|
||||
|
||||
const result = normalizeChartConfigurationFields(mixedConfig);
|
||||
|
||||
expect(result.groupByFieldMetadataId).toBe('pie-field');
|
||||
expect(result.orderBy).toBe(GraphOrderBy.FIELD_DESC);
|
||||
});
|
||||
});
|
||||
});
|
||||
+21
-5
@@ -1,6 +1,8 @@
|
||||
import { type BuildChartDrilldownQueryParamsInput } from '@/page-layout/widgets/graph/types/BuildChartDrilldownQueryParamsInput';
|
||||
import { buildFilterFromChartBucket } from '@/page-layout/widgets/graph/utils/buildFilterFromChartBucket';
|
||||
import { buildFilterQueryParams } from '@/page-layout/widgets/graph/utils/buildFilterQueryParams';
|
||||
import { buildSortsFromChartConfig } from '@/page-layout/widgets/graph/utils/buildSortsFromChartConfig';
|
||||
import { normalizeChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const buildChartDrilldownQueryParams = ({
|
||||
@@ -24,16 +26,21 @@ export const buildChartDrilldownQueryParams = ({
|
||||
});
|
||||
}
|
||||
|
||||
const primaryField = objectMetadataItem.fields.find(
|
||||
(field) => field.id === configuration.primaryAxisGroupByFieldMetadataId,
|
||||
);
|
||||
const { groupByFieldMetadataId, dateGranularity, groupBySubFieldName } =
|
||||
normalizeChartConfigurationFields(configuration);
|
||||
|
||||
const primaryField = isDefined(groupByFieldMetadataId)
|
||||
? objectMetadataItem.fields.find(
|
||||
(field) => field.id === groupByFieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (isDefined(primaryField)) {
|
||||
const primaryFilters = buildFilterFromChartBucket({
|
||||
fieldMetadataItem: primaryField,
|
||||
bucketRawValue: clickedData.primaryBucketRawValue,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity,
|
||||
subFieldName: configuration.primaryAxisGroupBySubFieldName,
|
||||
dateGranularity,
|
||||
subFieldName: groupBySubFieldName,
|
||||
timezone,
|
||||
});
|
||||
|
||||
@@ -45,6 +52,15 @@ export const buildChartDrilldownQueryParams = ({
|
||||
});
|
||||
}
|
||||
|
||||
const sorts = buildSortsFromChartConfig({
|
||||
configuration,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
sorts.forEach((sort) => {
|
||||
drilldownQueryParams.append(`sort[${sort.fieldName}]`, sort.direction);
|
||||
});
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
drilldownQueryParams.set('viewId', viewId);
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type NormalizedChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type ChartSort = {
|
||||
fieldName: string;
|
||||
direction: 'ASC' | 'DESC';
|
||||
};
|
||||
|
||||
type BuildSortsForChartFieldOrderByParams = {
|
||||
normalizedFields: NormalizedChartConfigurationFields;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const buildSortsForChartFieldOrderBy = ({
|
||||
normalizedFields,
|
||||
objectMetadataItem,
|
||||
}: BuildSortsForChartFieldOrderByParams): ChartSort | null => {
|
||||
const { groupByFieldMetadataId, groupBySubFieldName, orderBy } =
|
||||
normalizedFields;
|
||||
|
||||
if (
|
||||
orderBy !== GraphOrderBy.FIELD_ASC &&
|
||||
orderBy !== GraphOrderBy.FIELD_DESC
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primaryField = groupByFieldMetadataId
|
||||
? objectMetadataItem.fields.find(
|
||||
(field) => field.id === groupByFieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(primaryField)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldName = isNonEmptyString(groupBySubFieldName)
|
||||
? `${primaryField.name}.${groupBySubFieldName}`
|
||||
: primaryField.name;
|
||||
|
||||
return {
|
||||
fieldName,
|
||||
direction: orderBy === GraphOrderBy.FIELD_ASC ? 'ASC' : 'DESC',
|
||||
};
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type NormalizedChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
type ChartSort = {
|
||||
fieldName: string;
|
||||
direction: 'ASC' | 'DESC';
|
||||
};
|
||||
|
||||
type BuildSortsForChartValueOrderByParams = {
|
||||
normalizedFields: NormalizedChartConfigurationFields;
|
||||
aggregateFieldMetadataId: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const buildSortsForChartValueOrderBy = ({
|
||||
normalizedFields,
|
||||
aggregateFieldMetadataId,
|
||||
objectMetadataItem,
|
||||
}: BuildSortsForChartValueOrderByParams): ChartSort | null => {
|
||||
const { orderBy } = normalizedFields;
|
||||
|
||||
if (
|
||||
orderBy !== GraphOrderBy.VALUE_ASC &&
|
||||
orderBy !== GraphOrderBy.VALUE_DESC
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const aggregateField = objectMetadataItem.fields.find(
|
||||
(field) => field.id === aggregateFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(aggregateField)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fieldName: aggregateField.name,
|
||||
direction: orderBy === GraphOrderBy.VALUE_ASC ? 'ASC' : 'DESC',
|
||||
};
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { buildSortsForChartFieldOrderBy } from '@/page-layout/widgets/graph/utils/buildSortsForChartFieldOrderBy';
|
||||
import { buildSortsForChartValueOrderBy } from '@/page-layout/widgets/graph/utils/buildSortsForChartValueOrderBy';
|
||||
import { normalizeChartConfigurationFields } from '@/page-layout/widgets/graph/utils/normalizeChartConfigurationFields';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type LineChartConfiguration,
|
||||
type PieChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type ChartSort = {
|
||||
fieldName: string;
|
||||
direction: 'ASC' | 'DESC';
|
||||
};
|
||||
|
||||
type BuildSortsFromChartConfigParams = {
|
||||
configuration:
|
||||
| BarChartConfiguration
|
||||
| LineChartConfiguration
|
||||
| PieChartConfiguration;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const buildSortsFromChartConfig = ({
|
||||
configuration,
|
||||
objectMetadataItem,
|
||||
}: BuildSortsFromChartConfigParams): ChartSort[] => {
|
||||
const normalizedFields = normalizeChartConfigurationFields(configuration);
|
||||
|
||||
const fieldSort = buildSortsForChartFieldOrderBy({
|
||||
normalizedFields,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (isDefined(fieldSort)) {
|
||||
return [fieldSort];
|
||||
}
|
||||
|
||||
const valueSort = buildSortsForChartValueOrderBy({
|
||||
normalizedFields,
|
||||
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (isDefined(valueSort)) {
|
||||
return [valueSort];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type GraphOrderBy,
|
||||
type LineChartConfiguration,
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
type PieChartConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export type NormalizedChartConfigurationFields = {
|
||||
groupByFieldMetadataId?: string;
|
||||
groupBySubFieldName?: string | null;
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity | null;
|
||||
orderBy?: GraphOrderBy | null;
|
||||
};
|
||||
|
||||
export const normalizeChartConfigurationFields = (
|
||||
configuration:
|
||||
| BarChartConfiguration
|
||||
| LineChartConfiguration
|
||||
| PieChartConfiguration,
|
||||
): NormalizedChartConfigurationFields => {
|
||||
if (
|
||||
configuration.__typename === 'BarChartConfiguration' ||
|
||||
configuration.__typename === 'LineChartConfiguration'
|
||||
) {
|
||||
return {
|
||||
groupByFieldMetadataId: configuration.primaryAxisGroupByFieldMetadataId,
|
||||
groupBySubFieldName: configuration.primaryAxisGroupBySubFieldName,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity,
|
||||
orderBy: configuration.primaryAxisOrderBy,
|
||||
};
|
||||
}
|
||||
|
||||
if (configuration.__typename === 'PieChartConfiguration') {
|
||||
return {
|
||||
groupByFieldMetadataId: configuration.groupByFieldMetadataId,
|
||||
groupBySubFieldName: configuration.groupBySubFieldName,
|
||||
dateGranularity: configuration.dateGranularity,
|
||||
orderBy: configuration.orderBy,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { useHasFiltersInQueryParams } from '@/views/hooks/internal/useHasFiltersInQueryParams';
|
||||
import { useSortsFromQueryParams } from '@/views/hooks/internal/useSortsFromQueryParams';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
|
||||
export const QueryParamsCleanupEffect = () => {
|
||||
const { hasFiltersQueryParams } = useHasFiltersInQueryParams();
|
||||
const { hasSortsQueryParams, objectMetadataItem } = useSortsFromQueryParams();
|
||||
|
||||
const { currentView } = useGetCurrentViewOnly();
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [hasCleanedQueryParams, setHasCleanedQueryParams] = useState(false);
|
||||
|
||||
const currentViewObjectMetadataItemIsDifferentFromURLObjectMetadataItem =
|
||||
currentView?.objectMetadataId !== objectMetadataItem.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentViewObjectMetadataItemIsDifferentFromURLObjectMetadataItem ||
|
||||
hasCleanedQueryParams
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasFiltersQueryParams && !hasSortsQueryParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
|
||||
Array.from(newParams.keys()).forEach((key) => {
|
||||
if (
|
||||
key.startsWith('filter[') ||
|
||||
key.startsWith('filterGroup[') ||
|
||||
key.startsWith('sort[')
|
||||
) {
|
||||
newParams.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
setSearchParams(newParams, { replace: true });
|
||||
setHasCleanedQueryParams(true);
|
||||
}, [
|
||||
currentViewObjectMetadataItemIsDifferentFromURLObjectMetadataItem,
|
||||
hasFiltersQueryParams,
|
||||
hasSortsQueryParams,
|
||||
hasCleanedQueryParams,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useSortsFromQueryParams } from '@/views/hooks/internal/useSortsFromQueryParams';
|
||||
import { useApplyViewSortsToCurrentRecordSorts } from '@/views/hooks/useApplyViewSortsToCurrentRecordSorts';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
|
||||
export const QueryParamsSortsEffect = () => {
|
||||
const { hasSortsQueryParams, getSortsFromQueryParams, objectMetadataItem } =
|
||||
useSortsFromQueryParams();
|
||||
|
||||
const { currentView } = useGetCurrentViewOnly();
|
||||
|
||||
const { applyViewSortsToCurrentRecordSorts } =
|
||||
useApplyViewSortsToCurrentRecordSorts();
|
||||
|
||||
const currentViewObjectMetadataItemIsDifferentFromURLObjectMetadataItem =
|
||||
currentView?.objectMetadataId !== objectMetadataItem.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!hasSortsQueryParams ||
|
||||
currentViewObjectMetadataItemIsDifferentFromURLObjectMetadataItem
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortsFromParams = getSortsFromQueryParams();
|
||||
if (sortsFromParams.length > 0) {
|
||||
const viewSorts = sortsFromParams.map((sort) => ({
|
||||
...sort,
|
||||
viewId: currentView?.id ?? '',
|
||||
}));
|
||||
|
||||
applyViewSortsToCurrentRecordSorts(viewSorts);
|
||||
}
|
||||
}, [
|
||||
hasSortsQueryParams,
|
||||
getSortsFromQueryParams,
|
||||
applyViewSortsToCurrentRecordSorts,
|
||||
currentViewObjectMetadataItemIsDifferentFromURLObjectMetadataItem,
|
||||
currentView?.id,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/c
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { TopBar } from '@/ui/layout/top-bar/components/TopBar';
|
||||
import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect';
|
||||
import { QueryParamsSortsEffect } from '@/views/components/QueryParamsSortsEffect';
|
||||
import { ViewBarPageTitle } from '@/views/components/ViewBarPageTitle';
|
||||
import { ViewBarSkeletonLoader } from '@/views/components/ViewBarSkeletonLoader';
|
||||
import { ViewPickerDropdown } from '@/views/view-picker/components/ViewPickerDropdown';
|
||||
@@ -12,6 +13,7 @@ import { ViewPickerDropdown } from '@/views/view-picker/components/ViewPickerDro
|
||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||
import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId';
|
||||
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
|
||||
import { QueryParamsCleanupEffect } from '@/views/components/QueryParamsCleanupEffect';
|
||||
import { ViewBarAnyFieldFilterEffect } from '@/views/components/ViewBarAnyFieldFilterEffect';
|
||||
import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown';
|
||||
import { ViewBarRecordFieldEffect } from '@/views/components/ViewBarRecordFieldEffect';
|
||||
@@ -50,6 +52,8 @@ export const ViewBar = ({
|
||||
<ViewBarRecordFilterEffect />
|
||||
<ViewBarRecordSortEffect />
|
||||
<QueryParamsFiltersEffect />
|
||||
<QueryParamsSortsEffect />
|
||||
<QueryParamsCleanupEffect />
|
||||
<ViewBarPageTitle />
|
||||
<TopBar
|
||||
className={className}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import qs from 'qs';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { sortUrlQueryParamsSchema } from '@/views/schemas/sortUrlQueryParamsSchema';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ViewSortDirection } from '~/generated/graphql';
|
||||
|
||||
export const useSortsFromQueryParams = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { objectNamePlural = '' } = useParams();
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
objectNamePlural,
|
||||
});
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const queryParamsValidation = sortUrlQueryParamsSchema.safeParse(
|
||||
qs.parse(searchParams.toString()),
|
||||
);
|
||||
|
||||
const sortQueryParams = useMemo(
|
||||
() =>
|
||||
queryParamsValidation.success ? queryParamsValidation.data.sort : {},
|
||||
[queryParamsValidation],
|
||||
);
|
||||
|
||||
const hasSortsQueryParams =
|
||||
isDefined(sortQueryParams) && Object.entries(sortQueryParams).length > 0;
|
||||
|
||||
const getSortsFromQueryParams = useCallback((): RecordSort[] => {
|
||||
if (!hasSortsQueryParams) return [];
|
||||
|
||||
return Object.entries(sortQueryParams)
|
||||
.map(([fieldName, direction]) => {
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.name === fieldName,
|
||||
);
|
||||
|
||||
if (!fieldMetadataItem) return null;
|
||||
|
||||
return {
|
||||
id: `tmp-sort-${fieldName}`,
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
direction: direction as ViewSortDirection,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
}, [hasSortsQueryParams, sortQueryParams, objectMetadataItem.fields]);
|
||||
|
||||
return {
|
||||
hasSortsQueryParams,
|
||||
getSortsFromQueryParams,
|
||||
objectMetadataItem,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ViewSortDirection } from '~/generated/graphql';
|
||||
import z from 'zod';
|
||||
|
||||
export const sortUrlQueryParamsSchema = z.object({
|
||||
sort: z
|
||||
.record(z.string(), z.enum([ViewSortDirection.ASC, ViewSortDirection.DESC]))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type SortQueryParams = z.infer<typeof sortUrlQueryParamsSchema>;
|
||||
Reference in New Issue
Block a user