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:
nitin
2025-11-25 18:56:55 +05:30
committed by GitHub
parent 2028a8f9be
commit 3be3c4e965
35 changed files with 1097 additions and 245 deletions
@@ -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,
};
};