Files
twenty_crm/packages/twenty-front/src/modules/views/hooks/internal/useViewFromQueryParams.ts
Thomas Trompette 50e402af07 Use view filters operands in step filters + migrate to twenty-shared (#13137)
Step operand will more or less be the same as view filter operand. 

This PR:
- moves `ViewFilterOperand` to twenty-shared
- use it as step operand
- check what operand should be available based on the selected field
type in filter action
- rewrite the function that evaluates filters so it uses
ViewFilterOperand instead

ViewFilterOperand may be renamed in a future PR.
2025-07-10 08:36:37 +00:00

199 lines
7.3 KiB
TypeScript

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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
import { ViewFilter } from '@/views/types/ViewFilter';
import { relationFilterValueSchemaObject } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
import { isDefined } from 'twenty-shared/utils';
const filterQueryParamsSchema = z.object({
viewId: z.string().optional(),
filter: z
.record(
z.record(
z.nativeEnum(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,
};
};