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:
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user