From c605fa39216690b88d7fb3735e527753fff3c9c9 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 22 Nov 2024 19:25:30 +0100 Subject: [PATCH] fix: nlp pattern select+useFindFromCache --- .../inputs/AutoCompleteNlpPatternSelect.tsx | 159 ++++++++++++++++++ .../form/inputs/triggers/PatternInput.tsx | 15 +- frontend/src/hooks/crud/useFind.tsx | 30 +++- 3 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app-components/inputs/AutoCompleteNlpPatternSelect.tsx diff --git a/frontend/src/app-components/inputs/AutoCompleteNlpPatternSelect.tsx b/frontend/src/app-components/inputs/AutoCompleteNlpPatternSelect.tsx new file mode 100644 index 00000000..f024ab7d --- /dev/null +++ b/frontend/src/app-components/inputs/AutoCompleteNlpPatternSelect.tsx @@ -0,0 +1,159 @@ +/* + * Copyright © 2024 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { ChipTypeMap } from "@mui/material"; +import { AutocompleteProps } from "@mui/material/Autocomplete"; +import { forwardRef, useEffect, useRef } from "react"; + +import { useFind, useFindFromCache } from "@/hooks/crud/useFind"; +import { useInfiniteFind } from "@/hooks/crud/useInfiniteFind"; +import { useSearch } from "@/hooks/useSearch"; +import { EntityType, Format, QueryType } from "@/services/types"; +import { IEntityMapTypes } from "@/types/base.types"; +import { TFilterStringFields } from "@/types/search.types"; +import { generateId } from "@/utils/generateId"; + +import AutoCompleteSelect from "./AutoCompleteSelect"; + +type AutoCompleteEntitySelectProps< + Value, + Label extends keyof Value = keyof Value, + Multiple extends boolean | undefined = true, +> = Omit< + AutocompleteProps< + Value, + Multiple, + false, + false, + ChipTypeMap["defaultComponent"] + >, + "renderInput" | "options" | "value" | "defaultValue" +> & { + value?: Multiple extends true ? string[] : string | null; + label: string; + idKey?: string; + labelKey: Label; + entity: keyof IEntityMapTypes; + format: Format; + searchFields: string[]; + error?: boolean; + helperText?: string | null | undefined; + preprocess?: (data: Value[]) => Value[]; + noOptionsWarning?: string; + type: "entity" | "value"; +}; + +const AutoCompleteNlpPatternSelect = < + Value, + Label extends keyof Value = keyof Value, + Multiple extends boolean | undefined = true, +>( + { + label, + value, + entity, + format, + searchFields, + multiple, + onChange, + error, + helperText, + preprocess, + idKey = "id", + labelKey, + type, + ...rest + }: AutoCompleteEntitySelectProps, + ref, +) => { + useFind( + { entity: EntityType.NLP_ENTITY, format: Format.FULL }, + { hasCount: false }, + ); + const findNlpEntityFromCache = useFindFromCache(EntityType.NLP_ENTITY); + const { onSearch, searchPayload } = useSearch({ + $or: (searchFields as TFilterStringFields) || [idKey, labelKey], + }); + const idRef = useRef(generateId()); + const params = { + where: { + or: [ + ...(searchPayload.where.or || []), + ...(value + ? Array.isArray(value) + ? value.map((v) => { + return type === "entity" + ? { [type]: findNlpEntityFromCache({ name: v })?.[0]?.id } + : { [idKey]: v }; + }) + : [ + type === "entity" + ? { [type]: findNlpEntityFromCache({ name: value })?.[0]?.id } + : { [idKey]: value }, + ] + : []), + ], + }, + }; + const { data, isFetching, fetchNextPage } = useInfiniteFind( + { entity, format }, + { + params, + hasCount: false, + }, + { + keepPreviousData: true, + queryKey: [QueryType.collection, entity, `autocomplete/${idRef.current}`], + }, + ); + // flatten & filter unique + const flattenedData = data?.pages + ?.flat() + .filter( + (a, idx, self) => self.findIndex((b) => a[idKey] === b[idKey]) === idx, + ); + const options = + preprocess && flattenedData + ? preprocess((flattenedData || []) as unknown as Value[]) + : ((flattenedData || []) as Value[]); + + useEffect(() => { + fetchNextPage({ pageParam: params }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(searchPayload)]); + + return ( + + {...(options.length && { value })} + onChange={onChange} + label={label} + multiple={multiple} + ref={ref} + idKey={idKey} + labelKey={labelKey} + options={options || []} + onSearch={onSearch} + error={error} + helperText={helperText} + loading={isFetching} + {...rest} + /> + ); +}; + +AutoCompleteNlpPatternSelect.displayName = "AutoCompleteNlpPatternSelect"; + +export default forwardRef(AutoCompleteNlpPatternSelect) as unknown as < + Value, + Label extends keyof Value = keyof Value, + Multiple extends boolean | undefined = true, +>( + props: AutoCompleteEntitySelectProps & { + ref?: React.ForwardedRef; + }, +) => ReturnType; diff --git a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx index c0152bbe..48739c99 100644 --- a/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx +++ b/frontend/src/components/visual-editor/form/inputs/triggers/PatternInput.tsx @@ -11,6 +11,7 @@ import { FC, useEffect, useState } from "react"; import { RegisterOptions, useFormContext } from "react-hook-form"; import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect"; +import AutoCompleteNlpPatternSelect from "@/app-components/inputs/AutoCompleteNlpPatternSelect"; import { Input } from "@/app-components/inputs/Input"; import { RegexInput } from "@/app-components/inputs/RegexInput"; import { useGetFromCache } from "@/hooks/crud/useGet"; @@ -148,10 +149,11 @@ const PatternInput: FC = ({ {patternType === "nlp" ? ( - + value={(pattern as NlpPattern[]).map((v) => "value" in v && v.value ? v.value : v.entity, )} + type={(pattern as NlpPattern[]).map((v) => v.match)[0]} searchFields={["value", "label"]} entity={EntityType.NLP_VALUE} format={Format.FULL} @@ -164,7 +166,7 @@ const PatternInput: FC = ({ data.map((d) => { const entity = getNlpEntityFromCache(d.entity) as INlpEntity; - return d.value === "any" + return d.value === d.entity ? { match: "entity", entity: entity.name, @@ -180,7 +182,9 @@ const PatternInput: FC = ({ getOptionLabel={(option) => { const entity = getNlpEntityFromCache(option.entity) as INlpEntity; - return `${entity.name}=${option.value}`; + if (entity.name === option.value) { + return `${entity.name}=any`; + } else return `${entity.name}=${option.value}`; }} groupBy={(option) => { const entity = getNlpEntityFromCache(option.entity) as INlpEntity; @@ -206,14 +210,15 @@ const PatternInput: FC = ({ if (entity.lookups.includes("keywords")) { const exists = acc.find( - ({ value, id }) => value === "any" && id === entity.id, + ({ value, id }) => + value === entity.name && id === entity.id, ); if (!exists) { acc.push({ entity: entity.id, id: entity.id, - value: "any", + value: entity.name, } as INlpValue); } } diff --git a/frontend/src/hooks/crud/useFind.tsx b/frontend/src/hooks/crud/useFind.tsx index 4eae0130..53993fb8 100644 --- a/frontend/src/hooks/crud/useFind.tsx +++ b/frontend/src/hooks/crud/useFind.tsx @@ -6,7 +6,7 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { useQuery, UseQueryOptions } from "react-query"; +import { useQuery, useQueryClient, UseQueryOptions } from "react-query"; import { EntityType, @@ -17,6 +17,7 @@ import { import { IBaseSchema, IDynamicProps, + IEntityMapTypes, IFindConfigProps, POPULATE_BY_TYPE, TAllowedFormat, @@ -26,7 +27,7 @@ import { import { useEntityApiClient } from "../useApiClient"; import { usePagination } from "../usePagination"; -import { useNormalizeAndCache } from "./helpers"; +import { isSameEntity, useNormalizeAndCache } from "./helpers"; import { useCount } from "./useCount"; import { useGetFromCache } from "./useGet"; @@ -103,3 +104,28 @@ export const useFind = < }, }; }; + +export const useFindFromCache = < + E extends keyof IEntityMapTypes, + TData extends IBaseSchema = TType["basic"], +>( + entity: E, +) => { + const queryClient = useQueryClient(); + + return (criteria: Partial) => { + const queriesData = queryClient.getQueriesData({ + predicate: ({ queryKey }) => { + const [qType, qEntity] = queryKey; + + return qType === QueryType.item && isSameEntity(qEntity, entity); + }, + }); + + return queriesData + .reduce((acc, [, itemData]) => [...acc, itemData], [] as TData[]) + .filter((obj) => + Object.keys(criteria).every((key) => obj[key] === criteria[key]), + ); + }; +};