part 2 of filter/sort drill down from charts (#16013)
in this PR, we will handle simple filter translations (ie, fields that use the contains operand)
This commit is contained in:
+28
-10
@@ -3,7 +3,10 @@ import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/gr
|
||||
import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { getEffectiveGroupMode } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getEffectiveGroupMode';
|
||||
import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams';
|
||||
import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||
import { type ComputedDatum } from '@nivo/bar';
|
||||
import { lazy, Suspense } from 'react';
|
||||
@@ -40,6 +43,7 @@ export const GraphWidgetBarChartRenderer = ({
|
||||
layout,
|
||||
loading,
|
||||
hasTooManyGroups,
|
||||
formattedToRawLookup,
|
||||
objectMetadataItem,
|
||||
} = useGraphBarChartWidgetData({
|
||||
objectMetadataItemId: widget.objectMetadataId,
|
||||
@@ -48,6 +52,9 @@ export const GraphWidgetBarChartRenderer = ({
|
||||
|
||||
const navigate = useNavigate();
|
||||
const configuration = widget.configuration as BarChartConfiguration;
|
||||
const isPageLayoutInEditMode = useRecoilComponentValue(
|
||||
isPageLayoutInEditModeComponentState,
|
||||
);
|
||||
|
||||
const hasGroupByOnSecondaryAxis = isDefined(
|
||||
configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
@@ -68,16 +75,27 @@ export const GraphWidgetBarChartRenderer = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const handleBarClick = (_datum: ComputedDatum<BarChartDataItem>) => {
|
||||
return navigate(
|
||||
getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
},
|
||||
isDefined(indexViewId) ? { viewId: indexViewId } : undefined,
|
||||
),
|
||||
const handleBarClick = (datum: ComputedDatum<BarChartDataItem>) => {
|
||||
const displayValue = datum.data[indexBy];
|
||||
const rawValue = formattedToRawLookup.get(displayValue as string) ?? null;
|
||||
|
||||
const queryParams = buildChartDrilldownQueryParams({
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
clickedData: {
|
||||
primaryBucketRawValue: rawValue,
|
||||
},
|
||||
viewId: indexViewId,
|
||||
timezone: configuration.timezone ?? undefined,
|
||||
});
|
||||
|
||||
const url = getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: objectMetadataItem.namePlural },
|
||||
Object.fromEntries(queryParams),
|
||||
);
|
||||
|
||||
navigate(url);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -105,7 +123,7 @@ export const GraphWidgetBarChartRenderer = ({
|
||||
rangeMin={configuration.rangeMin ?? undefined}
|
||||
rangeMax={configuration.rangeMax ?? undefined}
|
||||
omitNullValues={configuration.omitNullValues ?? false}
|
||||
onBarClick={handleBarClick}
|
||||
onBarClick={isPageLayoutInEditMode ? undefined : handleBarClick}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
+7
-4
@@ -1,11 +1,11 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
|
||||
import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { type BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { transformGroupByDataToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData';
|
||||
import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { useMemo } from 'react';
|
||||
import { type BarChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
@@ -15,7 +15,7 @@ type UseGraphBarChartWidgetDataProps = {
|
||||
};
|
||||
|
||||
type UseGraphBarChartWidgetDataResult = {
|
||||
data: BarChartDataItem[];
|
||||
data: BarDatum[];
|
||||
indexBy: string;
|
||||
keys: string[];
|
||||
series: BarChartSeries[];
|
||||
@@ -26,7 +26,10 @@ type UseGraphBarChartWidgetDataResult = {
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
hasTooManyGroups: boolean;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
objectMetadataItem: ReturnType<
|
||||
typeof useObjectMetadataItemById
|
||||
>['objectMetadataItem'];
|
||||
};
|
||||
|
||||
// TODO: Remove this once backend returns total group count
|
||||
|
||||
+4
@@ -11,6 +11,7 @@ import { fillDateGapsInBarChartData } from '@/page-layout/widgets/graph/graphWid
|
||||
import { transformOneDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData';
|
||||
import { transformTwoDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformTwoDimensionalGroupByToBarChartData';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -38,6 +39,7 @@ type TransformGroupByDataToBarChartDataResult = {
|
||||
showDataLabels: boolean;
|
||||
layout?: BarChartLayout;
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
const EMPTY_BAR_CHART_RESULT: TransformGroupByDataToBarChartDataResult = {
|
||||
@@ -50,6 +52,7 @@ const EMPTY_BAR_CHART_RESULT: TransformGroupByDataToBarChartDataResult = {
|
||||
showDataLabels: false,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
hasTooManyGroups: false,
|
||||
formattedToRawLookup: new Map(),
|
||||
};
|
||||
|
||||
export const transformGroupByDataToBarChartData = ({
|
||||
@@ -195,5 +198,6 @@ export const transformGroupByDataToBarChartData = ({
|
||||
showDataLabels,
|
||||
layout,
|
||||
hasTooManyGroups: baseResult.hasTooManyGroups || dateRangeWasTruncated,
|
||||
formattedToRawLookup: baseResult.formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
|
||||
+15
@@ -7,6 +7,9 @@ import { type BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBa
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
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 { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
@@ -29,6 +32,7 @@ type TransformOneDimensionalGroupByToBarChartDataResult = {
|
||||
keys: string[];
|
||||
series: BarChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
export const transformOneDimensionalGroupByToBarChartData = ({
|
||||
@@ -53,6 +57,16 @@ export const transformOneDimensionalGroupByToBarChartData = ({
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
const limitedResults = rawResults.slice(0, BAR_CHART_MAXIMUM_NUMBER_OF_BARS);
|
||||
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: limitedResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
primaryAxisDateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
});
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
|
||||
const data: BarChartDataItem[] = limitedResults.map((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
|
||||
@@ -96,5 +110,6 @@ export const transformOneDimensionalGroupByToBarChartData = ({
|
||||
keys: [aggregateValueKey],
|
||||
series,
|
||||
hasTooManyGroups: rawResults.length > BAR_CHART_MAXIMUM_NUMBER_OF_BARS,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
|
||||
+17
@@ -7,6 +7,9 @@ import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarC
|
||||
import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortBarChartDataBySecondaryDimensionSum';
|
||||
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 { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
@@ -34,6 +37,7 @@ type TransformTwoDimensionalGroupByToBarChartDataResult = {
|
||||
keys: string[];
|
||||
series: BarChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
export const transformTwoDimensionalGroupByToBarChartData = ({
|
||||
@@ -54,6 +58,14 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
|
||||
const dataMap = new Map<string, BarChartDataItem>();
|
||||
const xValues = new Set<string>();
|
||||
const yValues = new Set<string>();
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: rawResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
primaryAxisDateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
});
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
|
||||
let hasTooManyGroups = false;
|
||||
|
||||
@@ -75,6 +87,10 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
|
||||
subFieldName: configuration.secondaryAxisGroupBySubFieldName ?? undefined,
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -151,5 +167,6 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
|
||||
keys,
|
||||
series,
|
||||
hasTooManyGroups,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
|
||||
+29
-8
@@ -2,7 +2,11 @@ import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/Char
|
||||
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
|
||||
import { LINE_CHART_IS_STACKED_DEFAULT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartIsStackedDefault';
|
||||
import { useGraphLineChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData';
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams';
|
||||
import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||
import { type LineSeries, type Point } from '@nivo/line';
|
||||
import { lazy, Suspense } from 'react';
|
||||
@@ -35,6 +39,7 @@ export const GraphWidgetLineChartRenderer = ({
|
||||
showDataLabels,
|
||||
hasTooManyGroups,
|
||||
loading,
|
||||
formattedToRawLookup,
|
||||
objectMetadataItem,
|
||||
} = useGraphLineChartWidgetData({
|
||||
objectMetadataItemId: widget.objectMetadataId,
|
||||
@@ -43,6 +48,9 @@ export const GraphWidgetLineChartRenderer = ({
|
||||
|
||||
const navigate = useNavigate();
|
||||
const configuration = widget.configuration as LineChartConfiguration;
|
||||
const isPageLayoutInEditMode = useRecoilComponentValue(
|
||||
isPageLayoutInEditModeComponentState,
|
||||
);
|
||||
|
||||
const hasGroupByOnSecondaryAxis = isDefined(
|
||||
configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
@@ -66,14 +74,27 @@ export const GraphWidgetLineChartRenderer = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const handlePointClick = (_point: Point<LineSeries>) => {
|
||||
return navigate(
|
||||
getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: objectMetadataItem.namePlural },
|
||||
isDefined(indexViewId) ? { viewId: indexViewId } : undefined,
|
||||
),
|
||||
const handlePointClick = (point: Point<LineSeries>) => {
|
||||
const xValue = (point.data as LineChartDataPoint).x;
|
||||
const rawValue = formattedToRawLookup.get(xValue as string) ?? null;
|
||||
|
||||
const queryParams = buildChartDrilldownQueryParams({
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
clickedData: {
|
||||
primaryBucketRawValue: rawValue,
|
||||
},
|
||||
viewId: indexViewId,
|
||||
timezone: configuration.timezone ?? undefined,
|
||||
});
|
||||
|
||||
const url = getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: objectMetadataItem.namePlural },
|
||||
Object.fromEntries(queryParams),
|
||||
);
|
||||
|
||||
navigate(url);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -97,7 +118,7 @@ export const GraphWidgetLineChartRenderer = ({
|
||||
omitNullValues={configuration.omitNullValues ?? false}
|
||||
groupMode={groupMode}
|
||||
displayType="shortNumber"
|
||||
onSliceClick={handlePointClick}
|
||||
onSliceClick={isPageLayoutInEditMode ? undefined : handlePointClick}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
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 { useMemo } from 'react';
|
||||
import { type LineChartConfiguration } from '~/generated/graphql';
|
||||
@@ -17,9 +17,12 @@ type UseGraphLineChartWidgetDataResult = {
|
||||
yAxisLabel?: string;
|
||||
showDataLabels: boolean;
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItem: ReturnType<
|
||||
typeof useObjectMetadataItemById
|
||||
>['objectMetadataItem'];
|
||||
};
|
||||
|
||||
export const useGraphLineChartWidgetData = ({
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import {
|
||||
type BarChartConfiguration,
|
||||
type LineChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type BuildChartDrilldownQueryParamsInput = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
configuration: BarChartConfiguration | LineChartConfiguration;
|
||||
clickedData: {
|
||||
primaryBucketRawValue: RawDimensionValue;
|
||||
};
|
||||
viewId?: string;
|
||||
timezone?: string;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type RawDimensionValue = string | number | Date | null;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type UrlFilterGroup } from './mapRecordFilterGroupToUrlFilterGroup';
|
||||
|
||||
export const appendNestedUrlFilterGroupsToQueryParams = (
|
||||
urlFilterGroups: UrlFilterGroup[],
|
||||
queryParamsPrefix: string,
|
||||
targetQueryParams: URLSearchParams,
|
||||
): void => {
|
||||
for (const [groupIndex, urlFilterGroup] of urlFilterGroups.entries()) {
|
||||
const currentGroupPrefix = `${queryParamsPrefix}[${groupIndex}]`;
|
||||
targetQueryParams.set(
|
||||
`${currentGroupPrefix}[operator]`,
|
||||
urlFilterGroup.operator,
|
||||
);
|
||||
|
||||
if (isDefined(urlFilterGroup.filters)) {
|
||||
for (const [filterIndex, urlFilter] of urlFilterGroup.filters.entries()) {
|
||||
targetQueryParams.set(
|
||||
`${currentGroupPrefix}[filters][${filterIndex}][field]`,
|
||||
urlFilter.field,
|
||||
);
|
||||
targetQueryParams.set(
|
||||
`${currentGroupPrefix}[filters][${filterIndex}][op]`,
|
||||
urlFilter.op,
|
||||
);
|
||||
targetQueryParams.set(
|
||||
`${currentGroupPrefix}[filters][${filterIndex}][value]`,
|
||||
urlFilter.value,
|
||||
);
|
||||
if (isDefined(urlFilter.subField)) {
|
||||
targetQueryParams.set(
|
||||
`${currentGroupPrefix}[filters][${filterIndex}][subField]`,
|
||||
urlFilter.subField,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(urlFilterGroup.groups)) {
|
||||
appendNestedUrlFilterGroupsToQueryParams(
|
||||
urlFilterGroup.groups,
|
||||
`${currentGroupPrefix}[groups]`,
|
||||
targetQueryParams,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
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 { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const buildChartDrilldownQueryParams = ({
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
clickedData,
|
||||
viewId,
|
||||
timezone,
|
||||
}: BuildChartDrilldownQueryParamsInput): URLSearchParams => {
|
||||
const drilldownQueryParams = new URLSearchParams();
|
||||
|
||||
if (isDefined(configuration.filter)) {
|
||||
const chartFilterParams = buildFilterQueryParams({
|
||||
recordFilters: configuration.filter.recordFilters ?? [],
|
||||
recordFilterGroups: configuration.filter.recordFilterGroups ?? [],
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
chartFilterParams.forEach((value, key) => {
|
||||
drilldownQueryParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
const primaryField = objectMetadataItem.fields.find(
|
||||
(field) => field.id === configuration.primaryAxisGroupByFieldMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(primaryField)) {
|
||||
const primaryFilters = buildFilterFromChartBucket({
|
||||
fieldMetadataItem: primaryField,
|
||||
bucketRawValue: clickedData.primaryBucketRawValue,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity,
|
||||
subFieldName: configuration.primaryAxisGroupBySubFieldName,
|
||||
timezone,
|
||||
});
|
||||
|
||||
primaryFilters.forEach((filter) => {
|
||||
drilldownQueryParams.append(
|
||||
`filter[${filter.fieldName}][${filter.operand}]`,
|
||||
filter.value,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
drilldownQueryParams.set('viewId', viewId);
|
||||
}
|
||||
|
||||
return drilldownQueryParams;
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
ViewFilterOperand,
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type ChartFilter = {
|
||||
fieldName: string;
|
||||
operand: ViewFilterOperand;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type BuildFilterFromChartBucketParams = {
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
bucketRawValue: unknown;
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity | null; // TODO: Will be used for date filtering
|
||||
subFieldName?: string | null;
|
||||
timezone?: string; // TODO: Will be used for date filtering
|
||||
};
|
||||
|
||||
const formatChartFilterValue = (
|
||||
fieldType: FieldMetadataType,
|
||||
bucketRawValue: unknown,
|
||||
operand: ViewFilterOperand,
|
||||
): string => {
|
||||
const stringValue = String(bucketRawValue);
|
||||
|
||||
const needsJsonArray =
|
||||
operand === ViewFilterOperand.IS &&
|
||||
[
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.UUID,
|
||||
FieldMetadataType.RELATION,
|
||||
].includes(fieldType);
|
||||
|
||||
return needsJsonArray ? JSON.stringify([stringValue]) : stringValue;
|
||||
};
|
||||
|
||||
export const buildFilterFromChartBucket = ({
|
||||
fieldMetadataItem,
|
||||
bucketRawValue,
|
||||
subFieldName,
|
||||
}: BuildFilterFromChartBucketParams): ChartFilter[] => {
|
||||
const fieldName = isNonEmptyString(subFieldName)
|
||||
? `${fieldMetadataItem.name}.${subFieldName}`
|
||||
: fieldMetadataItem.name;
|
||||
|
||||
if (!isDefined(bucketRawValue) || !isNonEmptyString(String(bucketRawValue))) {
|
||||
return [
|
||||
{
|
||||
fieldName,
|
||||
operand: ViewFilterOperand.IS_EMPTY,
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const availableOperands = getRecordFilterOperands({
|
||||
filterType: getFilterTypeFromFieldType(fieldMetadataItem.type),
|
||||
subFieldName: subFieldName ?? undefined,
|
||||
});
|
||||
|
||||
if (availableOperands.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const operand = availableOperands[0];
|
||||
|
||||
const value = formatChartFilterValue(
|
||||
fieldMetadataItem.type,
|
||||
bucketRawValue,
|
||||
operand,
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
fieldName,
|
||||
operand,
|
||||
value,
|
||||
},
|
||||
];
|
||||
};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { appendNestedUrlFilterGroupsToQueryParams } from './appendNestedUrlFilterGroupsToQueryParams';
|
||||
import { mapRecordFilterToUrlFilter } from './mapRecordFilterToUrlFilter';
|
||||
import { mapRecordFilterGroupToUrlFilterGroup } from './mapRecordFilterGroupToUrlFilterGroup';
|
||||
|
||||
export const buildFilterQueryParams = ({
|
||||
recordFilters = [],
|
||||
recordFilterGroups = [],
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
recordFilters?: RecordFilter[];
|
||||
recordFilterGroups?: RecordFilterGroup[];
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}): URLSearchParams => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
const rootGroup = recordFilterGroups.find(
|
||||
(group) => !isDefined(group.parentRecordFilterGroupId),
|
||||
);
|
||||
|
||||
if (isDefined(rootGroup)) {
|
||||
const urlFilterGroup = mapRecordFilterGroupToUrlFilterGroup({
|
||||
recordFilterGroupId: rootGroup.id,
|
||||
allRecordFilters: recordFilters,
|
||||
allRecordFilterGroups: recordFilterGroups,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (isDefined(urlFilterGroup)) {
|
||||
params.set('filterGroup[operator]', urlFilterGroup.operator);
|
||||
|
||||
if (isDefined(urlFilterGroup.filters)) {
|
||||
for (const [index, filter] of urlFilterGroup.filters.entries()) {
|
||||
params.set(`filterGroup[filters][${index}][field]`, filter.field);
|
||||
params.set(`filterGroup[filters][${index}][op]`, filter.op);
|
||||
params.set(`filterGroup[filters][${index}][value]`, filter.value);
|
||||
if (isDefined(filter.subField)) {
|
||||
params.set(
|
||||
`filterGroup[filters][${index}][subField]`,
|
||||
filter.subField,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(urlFilterGroup.groups)) {
|
||||
appendNestedUrlFilterGroupsToQueryParams(
|
||||
urlFilterGroup.groups,
|
||||
'filterGroup[groups]',
|
||||
params,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const parentlessFilters = recordFilters.filter(
|
||||
(filter) => !isDefined(filter.recordFilterGroupId),
|
||||
);
|
||||
|
||||
for (const filter of parentlessFilters) {
|
||||
const urlFilter = mapRecordFilterToUrlFilter({
|
||||
recordFilter: filter,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (isDefined(urlFilter)) {
|
||||
const fieldName = isDefined(urlFilter.subField)
|
||||
? `${urlFilter.field}.${urlFilter.subField}`
|
||||
: urlFilter.field;
|
||||
|
||||
params.append(`filter[${fieldName}][${urlFilter.op}]`, urlFilter.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { type FormattedDimensionValue } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
|
||||
export const buildFormattedToRawLookup = (
|
||||
formattedValues: FormattedDimensionValue[],
|
||||
): Map<string, RawDimensionValue> => {
|
||||
const lookup = new Map<string, RawDimensionValue>();
|
||||
|
||||
formattedValues.forEach(
|
||||
({ formattedPrimaryDimensionValue, rawPrimaryDimensionValue }) => {
|
||||
if (!lookup.has(formattedPrimaryDimensionValue)) {
|
||||
lookup.set(formattedPrimaryDimensionValue, rawPrimaryDimensionValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return lookup;
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ObjectRecordGroupByDateGranularity } from '~/generated/graphql';
|
||||
|
||||
type FormatPrimaryDimensionValuesParameters = {
|
||||
groupByRawResults: GroupByRawResult[];
|
||||
primaryAxisGroupByField: FieldMetadataItem;
|
||||
primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity;
|
||||
primaryAxisGroupBySubFieldName?: string;
|
||||
};
|
||||
|
||||
export type FormattedDimensionValue = {
|
||||
formattedPrimaryDimensionValue: string;
|
||||
rawPrimaryDimensionValue: RawDimensionValue;
|
||||
};
|
||||
|
||||
export const formatPrimaryDimensionValues = ({
|
||||
groupByRawResults,
|
||||
primaryAxisGroupByField,
|
||||
primaryAxisDateGranularity,
|
||||
primaryAxisGroupBySubFieldName,
|
||||
}: FormatPrimaryDimensionValuesParameters): FormattedDimensionValue[] => {
|
||||
return groupByRawResults.reduce<FormattedDimensionValue[]>(
|
||||
(accumulator, rawResult) => {
|
||||
const groupByDimensionValues = rawResult.groupByDimensionValues;
|
||||
|
||||
if (!isDefined(groupByDimensionValues?.[0])) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
const rawPrimaryDimensionValue =
|
||||
groupByDimensionValues[0] as RawDimensionValue;
|
||||
|
||||
const formattedPrimaryDimensionValue = formatDimensionValue({
|
||||
value: rawPrimaryDimensionValue,
|
||||
fieldMetadata: primaryAxisGroupByField,
|
||||
dateGranularity: primaryAxisDateGranularity,
|
||||
subFieldName: primaryAxisGroupBySubFieldName,
|
||||
});
|
||||
|
||||
return [
|
||||
...accumulator,
|
||||
{
|
||||
formattedPrimaryDimensionValue,
|
||||
rawPrimaryDimensionValue,
|
||||
},
|
||||
];
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
mapRecordFilterToUrlFilter,
|
||||
type UrlFilter,
|
||||
} from './mapRecordFilterToUrlFilter';
|
||||
|
||||
export type UrlFilterGroup = {
|
||||
operator: string;
|
||||
filters?: UrlFilter[];
|
||||
groups?: UrlFilterGroup[];
|
||||
};
|
||||
|
||||
export const mapRecordFilterGroupToUrlFilterGroup = ({
|
||||
recordFilterGroupId,
|
||||
allRecordFilters,
|
||||
allRecordFilterGroups,
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
recordFilterGroupId: string;
|
||||
allRecordFilters: RecordFilter[];
|
||||
allRecordFilterGroups: RecordFilterGroup[];
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}): UrlFilterGroup | null => {
|
||||
const currentGroup = allRecordFilterGroups.find(
|
||||
(group) => group.id === recordFilterGroupId,
|
||||
);
|
||||
|
||||
if (!isDefined(currentGroup)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlFilterGroup: UrlFilterGroup = {
|
||||
operator: currentGroup.logicalOperator,
|
||||
};
|
||||
|
||||
const filtersInGroup = allRecordFilters
|
||||
.filter((filter) => filter.recordFilterGroupId === recordFilterGroupId)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.positionInRecordFilterGroup ?? 0) -
|
||||
(b.positionInRecordFilterGroup ?? 0),
|
||||
);
|
||||
|
||||
if (filtersInGroup.length > 0) {
|
||||
const urlFilters = filtersInGroup
|
||||
.map((filter) =>
|
||||
mapRecordFilterToUrlFilter({
|
||||
recordFilter: filter,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
if (urlFilters.length > 0) {
|
||||
urlFilterGroup.filters = urlFilters;
|
||||
}
|
||||
}
|
||||
|
||||
const childGroups = allRecordFilterGroups
|
||||
.filter((group) => group.parentRecordFilterGroupId === recordFilterGroupId)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.positionInRecordFilterGroup ?? 0) -
|
||||
(b.positionInRecordFilterGroup ?? 0),
|
||||
);
|
||||
|
||||
if (childGroups.length > 0) {
|
||||
const urlChildGroups = childGroups
|
||||
.map((childGroup) =>
|
||||
mapRecordFilterGroupToUrlFilterGroup({
|
||||
recordFilterGroupId: childGroup.id,
|
||||
allRecordFilters,
|
||||
allRecordFilterGroups,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
if (urlChildGroups.length > 0) {
|
||||
urlFilterGroup.groups = urlChildGroups;
|
||||
}
|
||||
}
|
||||
|
||||
return urlFilterGroup;
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type UrlFilter = {
|
||||
field: string;
|
||||
op: string;
|
||||
value: string;
|
||||
subField?: string;
|
||||
};
|
||||
|
||||
export const mapRecordFilterToUrlFilter = ({
|
||||
recordFilter,
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
recordFilter: RecordFilter;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}): UrlFilter | null => {
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.id === recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlFilter: UrlFilter = {
|
||||
field: fieldMetadataItem.name,
|
||||
op: recordFilter.operand,
|
||||
value: recordFilter.value,
|
||||
};
|
||||
|
||||
if (isNonEmptyString(recordFilter.subFieldName)) {
|
||||
urlFilter.subField = recordFilter.subFieldName;
|
||||
}
|
||||
|
||||
return urlFilter;
|
||||
};
|
||||
+4
@@ -5,6 +5,7 @@ import { type ExtendedAggregateOperations } from '@/object-record/record-table/t
|
||||
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults';
|
||||
import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToLineChartData';
|
||||
import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToLineChartData';
|
||||
@@ -27,6 +28,7 @@ type TransformGroupByDataToLineChartDataResult = {
|
||||
yAxisLabel?: string;
|
||||
showDataLabels: boolean;
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
const EMPTY_LINE_CHART_RESULT: TransformGroupByDataToLineChartDataResult = {
|
||||
@@ -35,6 +37,7 @@ const EMPTY_LINE_CHART_RESULT: TransformGroupByDataToLineChartDataResult = {
|
||||
yAxisLabel: undefined,
|
||||
showDataLabels: false,
|
||||
hasTooManyGroups: false,
|
||||
formattedToRawLookup: new Map(),
|
||||
};
|
||||
|
||||
export const transformGroupByDataToLineChartData = ({
|
||||
@@ -137,5 +140,6 @@ export const transformGroupByDataToLineChartData = ({
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
showDataLabels,
|
||||
formattedToRawLookup: baseResult.formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
|
||||
+15
@@ -7,6 +7,9 @@ import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidget
|
||||
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 { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -25,6 +28,7 @@ type TransformOneDimensionalGroupByToLineChartDataParams = {
|
||||
type TransformOneDimensionalGroupByToLineChartDataResult = {
|
||||
series: LineChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
export const transformOneDimensionalGroupByToLineChartData = ({
|
||||
@@ -42,6 +46,16 @@ export const transformOneDimensionalGroupByToLineChartData = ({
|
||||
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
);
|
||||
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: limitedResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
primaryAxisDateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
});
|
||||
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
|
||||
const data: LineChartDataPoint[] = limitedResults
|
||||
.map((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
@@ -90,5 +104,6 @@ export const transformOneDimensionalGroupByToLineChartData = ({
|
||||
series,
|
||||
hasTooManyGroups:
|
||||
rawResults.length > LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
|
||||
+16
-1
@@ -6,6 +6,9 @@ import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidget
|
||||
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 { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { sortLineChartSeries } from '@/page-layout/widgets/graph/utils/sortLineChartSeries';
|
||||
@@ -26,6 +29,7 @@ type TransformTwoDimensionalGroupByToLineChartDataParams = {
|
||||
type TransformTwoDimensionalGroupByToLineChartDataResult = {
|
||||
series: LineChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
formattedToRawLookup: Map<string, RawDimensionValue>;
|
||||
};
|
||||
|
||||
export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
@@ -41,6 +45,14 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
const seriesMap = new Map<string, Map<string, number>>();
|
||||
const allXValues: string[] = [];
|
||||
const xValueSet = new Set<string>();
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
groupByRawResults: rawResults,
|
||||
primaryAxisGroupByField: groupByFieldX,
|
||||
primaryAxisDateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
primaryAxisGroupBySubFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
});
|
||||
const formattedToRawLookup = buildFormattedToRawLookup(formattedValues);
|
||||
let hasTooManyGroups = false;
|
||||
|
||||
rawResults.forEach((result) => {
|
||||
@@ -70,8 +82,10 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
allXValues.push(xValue);
|
||||
}
|
||||
|
||||
const seriesRawValue = dimensionValues[1];
|
||||
|
||||
const seriesKey = formatDimensionValue({
|
||||
value: dimensionValues[1],
|
||||
value: seriesRawValue,
|
||||
fieldMetadata: groupByFieldY,
|
||||
dateGranularity:
|
||||
configuration.secondaryAxisGroupByDateGranularity ?? undefined,
|
||||
@@ -120,5 +134,6 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
return {
|
||||
series,
|
||||
hasTooManyGroups,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,20 +1,44 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
import { useApplyViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyViewFiltersToCurrentRecordFilters';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useFiltersFromQueryParams } from '@/views/hooks/internal/useFiltersFromQueryParams';
|
||||
import { useHasFiltersInQueryParams } from '@/views/hooks/internal/useHasFiltersInQueryParams';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { useMapViewFiltersToFilters } from '@/views/hooks/useMapViewFiltersToFilters';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const QueryParamsFiltersEffect = () => {
|
||||
const {
|
||||
hasFiltersQueryParams,
|
||||
getFiltersFromQueryParams,
|
||||
objectMetadataItem,
|
||||
} = useViewFromQueryParams();
|
||||
const { getFiltersFromQueryParams, getFilterGroupsFromQueryParams } =
|
||||
useFiltersFromQueryParams();
|
||||
const { hasFiltersQueryParams } = useHasFiltersInQueryParams();
|
||||
|
||||
const { objectNamePlural = '' } = useParams();
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
objectNamePlural,
|
||||
});
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { currentView } = useGetCurrentViewOnly();
|
||||
|
||||
const { applyViewFiltersToCurrentRecordFilters } =
|
||||
useApplyViewFiltersToCurrentRecordFilters();
|
||||
const { mapViewFiltersToRecordFilters } = useMapViewFiltersToFilters();
|
||||
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
const setCurrentRecordFilters = useSetRecoilComponentState(
|
||||
currentRecordFiltersComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
const setCurrentRecordFilterGroups = useSetRecoilComponentState(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const currentViewObjectMetadataItemIsDifferentFromURLObjectMetadataItem =
|
||||
currentView?.objectMetadataId !== objectMetadataItem.id;
|
||||
@@ -27,17 +51,49 @@ export const QueryParamsFiltersEffect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
getFiltersFromQueryParams().then((filtersFromParams) => {
|
||||
if (Array.isArray(filtersFromParams)) {
|
||||
applyViewFiltersToCurrentRecordFilters(filtersFromParams);
|
||||
const loadFiltersFromQueryParams = async () => {
|
||||
const [filtersFromParams, filterGroupsFromParams] = await Promise.all([
|
||||
getFiltersFromQueryParams(),
|
||||
getFilterGroupsFromQueryParams(),
|
||||
]);
|
||||
|
||||
const allRecordFilters = [];
|
||||
|
||||
if (
|
||||
isDefined(filterGroupsFromParams) &&
|
||||
filterGroupsFromParams.recordFilters.length > 0
|
||||
) {
|
||||
allRecordFilters.push(...filterGroupsFromParams.recordFilters);
|
||||
}
|
||||
});
|
||||
|
||||
if (Array.isArray(filtersFromParams) && filtersFromParams.length > 0) {
|
||||
const simpleRecordFilters =
|
||||
mapViewFiltersToRecordFilters(filtersFromParams);
|
||||
allRecordFilters.push(...simpleRecordFilters);
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(filterGroupsFromParams) &&
|
||||
filterGroupsFromParams.recordFilterGroups.length > 0
|
||||
) {
|
||||
setCurrentRecordFilterGroups(filterGroupsFromParams.recordFilterGroups);
|
||||
}
|
||||
|
||||
if (allRecordFilters.length > 0) {
|
||||
setCurrentRecordFilters(allRecordFilters);
|
||||
}
|
||||
};
|
||||
|
||||
loadFiltersFromQueryParams();
|
||||
}, [
|
||||
currentViewObjectMetadataItemIsDifferentFromURLObjectMetadataItem,
|
||||
applyViewFiltersToCurrentRecordFilters,
|
||||
mapViewFiltersToRecordFilters,
|
||||
getFiltersFromQueryParams,
|
||||
getFilterGroupsFromQueryParams,
|
||||
hasFiltersQueryParams,
|
||||
setCurrentRecordFilterGroups,
|
||||
setCurrentRecordFilters,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { UPDATE_VIEW_BUTTON_DROPDOWN_ID } from '@/views/constants/UpdateViewButtonDropdownId';
|
||||
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
import { useHasFiltersInQueryParams } from '@/views/hooks/internal/useHasFiltersInQueryParams';
|
||||
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
||||
import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
|
||||
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
||||
@@ -85,7 +85,7 @@ export const UpdateViewButtonGroup = () => {
|
||||
await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
|
||||
};
|
||||
|
||||
const { hasFiltersQueryParams } = useViewFromQueryParams();
|
||||
const { hasFiltersQueryParams } = useHasFiltersInQueryParams();
|
||||
|
||||
const { viewFilterGroupsAreDifferentFromRecordFilterGroups } =
|
||||
useAreViewFilterGroupsDifferentFromRecordFilterGroups();
|
||||
|
||||
@@ -9,7 +9,6 @@ import { AdvancedFilterDropdownButton } from '@/views/components/AdvancedFilterD
|
||||
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
||||
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
||||
import { ViewBarDetailsAddFilterButton } from '@/views/components/ViewBarDetailsAddFilterButton';
|
||||
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
@@ -26,6 +25,7 @@ import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDrop
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { AnyFieldSearchDropdownButton } from '@/views/components/AnyFieldSearchDropdownButton';
|
||||
import { ANY_FIELD_SEARCH_DROPDOWN_ID } from '@/views/constants/AnyFieldSearchDropdownId';
|
||||
import { useHasFiltersInQueryParams } from '@/views/hooks/internal/useHasFiltersInQueryParams';
|
||||
import { useApplyCurrentViewAnyFieldFilterToAnyFieldFilter } from '@/views/hooks/useApplyCurrentViewAnyFieldFilterToAnyFieldFilter';
|
||||
import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
|
||||
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
||||
@@ -109,7 +109,7 @@ export const ViewBarDetails = ({
|
||||
isViewBarExpandedComponentState,
|
||||
);
|
||||
|
||||
const { hasFiltersQueryParams } = useViewFromQueryParams();
|
||||
const { hasFiltersQueryParams } = useHasFiltersInQueryParams();
|
||||
|
||||
const currentRecordFilterGroups = useRecoilComponentValue(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { isNonEmptyString, isObject } from '@sniptt/guards';
|
||||
import qs from 'qs';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { filterUrlQueryParamsSchema } from '@/views/schemas/filterUrlQueryParamsSchema';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { deserializeUrlRecursiveFilterGroup } from '@/views/utils/deserializeUrlRecursiveFilterGroup';
|
||||
import { splitFieldNameIntoBaseAndSubField } from '@/views/utils/splitFieldNameIntoBaseAndSubField';
|
||||
import { useCallback } from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { type ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { isDefined, isExpectedSubFieldName } from 'twenty-shared/utils';
|
||||
|
||||
export const useFiltersFromQueryParams = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { objectNamePlural = '' } = useParams();
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
objectNamePlural,
|
||||
});
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const queryParamsValidation = filterUrlQueryParamsSchema.safeParse(
|
||||
qs.parse(searchParams.toString()),
|
||||
);
|
||||
|
||||
const getFiltersFromQueryParams = useCallback(async (): Promise<
|
||||
ViewFilter[]
|
||||
> => {
|
||||
if (!queryParamsValidation.success) return [];
|
||||
|
||||
const filterQueryParams = queryParamsValidation.data.filter;
|
||||
|
||||
if (
|
||||
!isDefined(filterQueryParams) ||
|
||||
Object.entries(filterQueryParams).length === 0
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const promises: Promise<ViewFilter | null>[] = [];
|
||||
|
||||
for (const [fieldName, filterFromURL] of Object.entries(
|
||||
filterQueryParams,
|
||||
)) {
|
||||
for (const [filterOperandFromURL, filterValueFromURL] of Object.entries(
|
||||
filterFromURL,
|
||||
)) {
|
||||
const promise = (async (): Promise<ViewFilter | null> => {
|
||||
const { baseFieldName, subFieldName } =
|
||||
splitFieldNameIntoBaseAndSubField(fieldName);
|
||||
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.name === baseFieldName,
|
||||
);
|
||||
|
||||
if (!fieldMetadataItem) return null;
|
||||
|
||||
if (isDefined(subFieldName) && isNonEmptyString(subFieldName)) {
|
||||
if (!isCompositeFieldType(fieldMetadataItem.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!isExpectedSubFieldName(
|
||||
fieldMetadataItem.type as Parameters<
|
||||
typeof isExpectedSubFieldName
|
||||
>[0],
|
||||
subFieldName as Parameters<typeof isExpectedSubFieldName>[1],
|
||||
subFieldName,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const filterValueAsString =
|
||||
Array.isArray(filterValueFromURL) || isObject(filterValueFromURL)
|
||||
? JSON.stringify(filterValueFromURL)
|
||||
: (filterValueFromURL as string);
|
||||
|
||||
const displayValue = filterValueAsString;
|
||||
|
||||
const filterId = `tmp-${[
|
||||
fieldName,
|
||||
filterOperandFromURL,
|
||||
filterValueFromURL,
|
||||
].join('-')}`;
|
||||
|
||||
return {
|
||||
__typename: 'ViewFilter',
|
||||
id: filterId,
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
operand: filterOperandFromURL as ViewFilterOperand,
|
||||
value: filterValueAsString,
|
||||
displayValue,
|
||||
subFieldName: subFieldName
|
||||
? (subFieldName as ViewFilter['subFieldName'])
|
||||
: undefined,
|
||||
};
|
||||
})();
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
}
|
||||
|
||||
return (await Promise.all(promises)).filter(isDefined);
|
||||
}, [queryParamsValidation, objectMetadataItem.fields]);
|
||||
|
||||
const filterGroupQueryParams = queryParamsValidation.success
|
||||
? queryParamsValidation.data.filterGroup
|
||||
: undefined;
|
||||
|
||||
const getFilterGroupsFromQueryParams = useCallback(async (): Promise<{
|
||||
recordFilters: RecordFilter[];
|
||||
recordFilterGroups: RecordFilterGroup[];
|
||||
}> => {
|
||||
if (!isDefined(filterGroupQueryParams)) {
|
||||
return { recordFilters: [], recordFilterGroups: [] };
|
||||
}
|
||||
|
||||
return deserializeUrlRecursiveFilterGroup({
|
||||
urlRecursiveFilterGroup: filterGroupQueryParams as Parameters<
|
||||
typeof deserializeUrlRecursiveFilterGroup
|
||||
>[0]['urlRecursiveFilterGroup'],
|
||||
objectMetadataItem,
|
||||
positionInParent: 0,
|
||||
});
|
||||
}, [filterGroupQueryParams, objectMetadataItem]);
|
||||
|
||||
return {
|
||||
getFiltersFromQueryParams,
|
||||
getFilterGroupsFromQueryParams,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import qs from 'qs';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { filterUrlQueryParamsSchema } from '@/views/schemas/filterUrlQueryParamsSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useHasFiltersInQueryParams = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const queryParamsValidation = filterUrlQueryParamsSchema.safeParse(
|
||||
qs.parse(searchParams.toString()),
|
||||
);
|
||||
|
||||
const filterQueryParams = queryParamsValidation.success
|
||||
? queryParamsValidation.data.filter
|
||||
: {};
|
||||
|
||||
const filterGroupQueryParams = queryParamsValidation.success
|
||||
? queryParamsValidation.data.filterGroup
|
||||
: undefined;
|
||||
|
||||
const hasFiltersQueryParams =
|
||||
(isDefined(filterQueryParams) &&
|
||||
Object.entries(filterQueryParams).length > 0) ||
|
||||
isDefined(filterGroupQueryParams);
|
||||
|
||||
return {
|
||||
hasFiltersQueryParams,
|
||||
};
|
||||
};
|
||||
@@ -1,201 +0,0 @@
|
||||
import { isNonEmptyString, isObject } from '@sniptt/guards';
|
||||
import qs from 'qs';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import z from 'zod';
|
||||
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
relationFilterValueSchemaObject,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
const filterQueryParamsSchema = z.object({
|
||||
viewId: z.string().optional(),
|
||||
filter: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.partialRecord(
|
||||
z.enum(ViewFilterOperand),
|
||||
z.string().or(z.array(z.string())).or(relationFilterValueSchemaObject),
|
||||
),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type FilterQueryParams = z.infer<typeof filterQueryParamsSchema>;
|
||||
|
||||
export const useViewFromQueryParams = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { objectNamePlural = '' } = useParams();
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
objectNamePlural,
|
||||
});
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const queryParamsValidation = filterQueryParamsSchema.safeParse(
|
||||
qs.parse(searchParams.toString()),
|
||||
);
|
||||
|
||||
const filterQueryParams = useMemo(
|
||||
() =>
|
||||
queryParamsValidation.success ? queryParamsValidation.data.filter : {},
|
||||
[queryParamsValidation],
|
||||
);
|
||||
const viewIdQueryParam = useMemo(
|
||||
() =>
|
||||
queryParamsValidation.success
|
||||
? queryParamsValidation.data.viewId
|
||||
: undefined,
|
||||
[queryParamsValidation],
|
||||
);
|
||||
|
||||
const hasFiltersQueryParams =
|
||||
isDefined(filterQueryParams) &&
|
||||
Object.entries(filterQueryParams).length > 0;
|
||||
|
||||
const getFiltersFromQueryParams = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
if (!hasFiltersQueryParams) return [];
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
Object.entries(filterQueryParams).map<Promise<ViewFilter | null>>(
|
||||
async ([fieldName, filterFromURL]) => {
|
||||
const [filterOperandFromURL, filterValueFromURL] =
|
||||
Object.entries(filterFromURL)[0];
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.name === fieldName,
|
||||
);
|
||||
|
||||
if (!fieldMetadataItem) return null;
|
||||
|
||||
const relationObjectMetadataNameSingular =
|
||||
fieldMetadataItem.relation?.targetObjectMetadata
|
||||
?.nameSingular;
|
||||
|
||||
const relationObjectMetadataNamePlural =
|
||||
fieldMetadataItem.relation?.targetObjectMetadata?.namePlural;
|
||||
|
||||
const relationObjectMetadataItem =
|
||||
relationObjectMetadataNameSingular
|
||||
? snapshot
|
||||
.getLoadable(
|
||||
objectMetadataItemFamilySelector({
|
||||
objectName: relationObjectMetadataNameSingular,
|
||||
objectNameType: 'singular',
|
||||
}),
|
||||
)
|
||||
.getValue()
|
||||
: null;
|
||||
|
||||
const satisfiesRelationFilterSchema =
|
||||
relationFilterValueSchemaObject.safeParse(
|
||||
filterValueFromURL,
|
||||
)?.success;
|
||||
|
||||
const relationRecordNames = [];
|
||||
|
||||
if (
|
||||
isNonEmptyString(relationObjectMetadataNamePlural) &&
|
||||
isDefined(relationObjectMetadataItem) &&
|
||||
(Array.isArray(filterValueFromURL) ||
|
||||
satisfiesRelationFilterSchema)
|
||||
) {
|
||||
const queryResult = await apolloCoreClient.query<
|
||||
Record<string, { edges: { node: ObjectRecord }[] }>
|
||||
>({
|
||||
query: generateFindManyRecordsQuery({
|
||||
objectMetadataItem: relationObjectMetadataItem,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}),
|
||||
variables: {
|
||||
filter: {
|
||||
id: {
|
||||
in: satisfiesRelationFilterSchema
|
||||
? (
|
||||
filterValueFromURL as {
|
||||
selectedRecordIds: string[];
|
||||
}
|
||||
)?.selectedRecordIds
|
||||
: filterValueFromURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const relationRecordNamesFromQuery = queryResult.data?.[
|
||||
relationObjectMetadataNamePlural
|
||||
]?.edges.map(
|
||||
({ node: record }) =>
|
||||
getObjectRecordIdentifier({
|
||||
objectMetadataItem: relationObjectMetadataItem,
|
||||
record,
|
||||
}).name,
|
||||
);
|
||||
|
||||
relationRecordNames.push(...relationRecordNamesFromQuery);
|
||||
}
|
||||
|
||||
const filterValueAsString =
|
||||
Array.isArray(filterValueFromURL) ||
|
||||
isObject(filterValueFromURL)
|
||||
? JSON.stringify(filterValueFromURL)
|
||||
: filterValueFromURL;
|
||||
|
||||
return {
|
||||
__typename: 'ViewFilter',
|
||||
id: `tmp-${[
|
||||
fieldName,
|
||||
filterOperandFromURL,
|
||||
filterValueFromURL,
|
||||
].join('-')}`,
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
operand: filterOperandFromURL as ViewFilterOperand,
|
||||
value: filterValueAsString,
|
||||
displayValue:
|
||||
relationRecordNames?.join(', ') ?? filterValueAsString,
|
||||
persistAction: 'NONE',
|
||||
};
|
||||
},
|
||||
),
|
||||
)
|
||||
).filter(isDefined);
|
||||
},
|
||||
[
|
||||
apolloCoreClient,
|
||||
filterQueryParams,
|
||||
hasFiltersQueryParams,
|
||||
objectMetadataItem.fields,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
viewIdQueryParam,
|
||||
hasFiltersQueryParams,
|
||||
getFiltersFromQueryParams,
|
||||
objectMetadataItem,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { relationFilterValueSchemaObject } from 'twenty-shared/utils';
|
||||
import z from 'zod';
|
||||
import { urlRecursiveFilterGroupSchema } from './urlRecursiveFilterGroupSchema';
|
||||
|
||||
export const filterUrlQueryParamsSchema = z.object({
|
||||
filter: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.partialRecord(
|
||||
z.enum(ViewFilterOperand),
|
||||
z.string().or(z.array(z.string())).or(relationFilterValueSchemaObject),
|
||||
),
|
||||
)
|
||||
.optional(),
|
||||
filterGroup: urlRecursiveFilterGroupSchema.optional(),
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { RecordFilterGroupLogicalOperator } from 'twenty-shared/types';
|
||||
import z from 'zod';
|
||||
import { urlSingleFilterSchema } from './urlSingleFilterSchema';
|
||||
|
||||
type UrlRecursiveFilterGroupSchemaType = {
|
||||
operator: RecordFilterGroupLogicalOperator;
|
||||
filters?: z.infer<typeof urlSingleFilterSchema>[];
|
||||
groups?: UrlRecursiveFilterGroupSchemaType[];
|
||||
};
|
||||
|
||||
export const urlRecursiveFilterGroupSchema: z.ZodType<UrlRecursiveFilterGroupSchemaType> =
|
||||
z.lazy(() =>
|
||||
z.object({
|
||||
operator: z.enum(RecordFilterGroupLogicalOperator),
|
||||
filters: z.array(urlSingleFilterSchema).optional(),
|
||||
groups: z.array(urlRecursiveFilterGroupSchema).optional(),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
import z from 'zod';
|
||||
|
||||
export const urlSingleFilterSchema = z.object({
|
||||
field: z.string(),
|
||||
op: z.enum(ViewFilterOperand),
|
||||
value: z.string(),
|
||||
subField: z.string().optional(),
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||
|
||||
export type UrlFilterDeserializationResult = {
|
||||
recordFilters: RecordFilter[];
|
||||
recordFilterGroups: RecordFilterGroup[];
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type urlRecursiveFilterGroupSchema } from '@/views/schemas/urlRecursiveFilterGroupSchema';
|
||||
import { type z } from 'zod';
|
||||
|
||||
export type UrlRecursiveFilterGroup = z.infer<
|
||||
typeof urlRecursiveFilterGroupSchema
|
||||
>;
|
||||
@@ -0,0 +1,4 @@
|
||||
import { type urlSingleFilterSchema } from '@/views/schemas/urlSingleFilterSchema';
|
||||
import { type z } from 'zod';
|
||||
|
||||
export type UrlSingleFilter = z.infer<typeof urlSingleFilterSchema>;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { type UrlSingleFilter } from '@/views/types/UrlSingleFilter';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined, isExpectedSubFieldName } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const convertUrlSingleFilterToRecordFilter = ({
|
||||
urlSingleFilter,
|
||||
objectMetadataItem,
|
||||
recordFilterGroupId,
|
||||
positionInGroup,
|
||||
}: {
|
||||
urlSingleFilter: UrlSingleFilter;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
recordFilterGroupId?: string;
|
||||
positionInGroup: number;
|
||||
}): RecordFilter | null => {
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.name === urlSingleFilter.field,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNonEmptyString(urlSingleFilter.subField)) {
|
||||
if (!isCompositeFieldType(fieldMetadataItem.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!isExpectedSubFieldName(
|
||||
fieldMetadataItem.type as Parameters<typeof isExpectedSubFieldName>[0],
|
||||
urlSingleFilter.subField as Parameters<
|
||||
typeof isExpectedSubFieldName
|
||||
>[1],
|
||||
urlSingleFilter.subField,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const displayValue = urlSingleFilter.value;
|
||||
|
||||
const recordFilter: RecordFilter = {
|
||||
id: uuidv4(),
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
value: urlSingleFilter.value,
|
||||
displayValue,
|
||||
type: fieldMetadataItem.type as RecordFilter['type'],
|
||||
operand: urlSingleFilter.op,
|
||||
label: fieldMetadataItem.label,
|
||||
positionInRecordFilterGroup: positionInGroup,
|
||||
};
|
||||
|
||||
if (isDefined(recordFilterGroupId)) {
|
||||
recordFilter.recordFilterGroupId = recordFilterGroupId;
|
||||
}
|
||||
|
||||
if (isNonEmptyString(urlSingleFilter.subField)) {
|
||||
recordFilter.subFieldName =
|
||||
urlSingleFilter.subField as RecordFilter['subFieldName'];
|
||||
}
|
||||
|
||||
return recordFilter;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { type UrlFilterDeserializationResult } from '@/views/types/UrlFilterDeserializationResult';
|
||||
import { type UrlRecursiveFilterGroup } from '@/views/types/UrlRecursiveFilterGroup';
|
||||
import { convertUrlSingleFilterToRecordFilter } from '@/views/utils/convertUrlSingleFilterToRecordFilter';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const deserializeUrlRecursiveFilterGroup = ({
|
||||
urlRecursiveFilterGroup,
|
||||
objectMetadataItem,
|
||||
parentGroupId,
|
||||
positionInParent,
|
||||
}: {
|
||||
urlRecursiveFilterGroup: UrlRecursiveFilterGroup;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
parentGroupId?: string;
|
||||
positionInParent: number;
|
||||
}): UrlFilterDeserializationResult => {
|
||||
const groupId = uuidv4();
|
||||
const recordFilters: RecordFilter[] = [];
|
||||
const recordFilterGroups: RecordFilterGroup[] = [];
|
||||
const groupFilters: RecordFilter[] = [];
|
||||
|
||||
let positionCounter = 0;
|
||||
|
||||
if (isDefined(urlRecursiveFilterGroup.filters)) {
|
||||
for (const urlSingleFilter of urlRecursiveFilterGroup.filters) {
|
||||
const recordFilter = convertUrlSingleFilterToRecordFilter({
|
||||
urlSingleFilter,
|
||||
objectMetadataItem,
|
||||
recordFilterGroupId: groupId,
|
||||
positionInGroup: positionCounter++,
|
||||
});
|
||||
|
||||
if (isDefined(recordFilter)) {
|
||||
groupFilters.push(recordFilter);
|
||||
recordFilters.push(recordFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const childGroupResults: RecordFilterGroup[] = [];
|
||||
if (isDefined(urlRecursiveFilterGroup.groups)) {
|
||||
for (const childUrlGroup of urlRecursiveFilterGroup.groups) {
|
||||
const childResult = deserializeUrlRecursiveFilterGroup({
|
||||
urlRecursiveFilterGroup: childUrlGroup,
|
||||
objectMetadataItem,
|
||||
parentGroupId: groupId,
|
||||
positionInParent: positionCounter++,
|
||||
});
|
||||
|
||||
if (
|
||||
childResult.recordFilters.length > 0 ||
|
||||
childResult.recordFilterGroups.length > 0
|
||||
) {
|
||||
recordFilters.push(...childResult.recordFilters);
|
||||
childGroupResults.push(...childResult.recordFilterGroups);
|
||||
recordFilterGroups.push(...childResult.recordFilterGroups);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasContent = groupFilters.length > 0 || childGroupResults.length > 0;
|
||||
|
||||
if (hasContent) {
|
||||
const currentGroup: RecordFilterGroup = {
|
||||
id: groupId,
|
||||
logicalOperator: urlRecursiveFilterGroup.operator,
|
||||
positionInRecordFilterGroup: positionInParent,
|
||||
};
|
||||
|
||||
if (isDefined(parentGroupId)) {
|
||||
currentGroup.parentRecordFilterGroupId = parentGroupId;
|
||||
}
|
||||
|
||||
recordFilterGroups.push(currentGroup);
|
||||
}
|
||||
|
||||
return {
|
||||
recordFilters,
|
||||
recordFilterGroups,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export const splitFieldNameIntoBaseAndSubField = (
|
||||
fieldName: string,
|
||||
): {
|
||||
baseFieldName: string;
|
||||
subFieldName?: string;
|
||||
} => {
|
||||
const fieldParts = fieldName.split('.');
|
||||
const baseFieldName = fieldParts[0];
|
||||
const subFieldName =
|
||||
fieldParts.length > 1 ? fieldParts.slice(1).join('.') : undefined;
|
||||
|
||||
return {
|
||||
baseFieldName,
|
||||
subFieldName,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user