fix: autocomplete entity

This commit is contained in:
Mohamed Marrouchi 2024-10-08 20:02:21 +01:00
parent d089506579
commit bd7a850d87
3 changed files with 131 additions and 58 deletions

View File

@ -8,13 +8,14 @@
import { ChipTypeMap } from "@mui/material";
import { AutocompleteProps } from "@mui/material/Autocomplete";
import { forwardRef, useEffect, useState } from "react";
import { forwardRef, useEffect, useRef } from "react";
import { useFind } from "@/hooks/crud/useFind";
import { useInfiniteFind } from "@/hooks/crud/useInfiniteFind";
import { useSearch } from "@/hooks/useSearch";
import { Format } from "@/services/types";
import { 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";
@ -62,75 +63,53 @@ const AutoCompleteEntitySelect = <
helperText,
preprocess,
idKey = "id",
labelKey,
...rest
}: AutoCompleteEntitySelectProps<Value, Label, Multiple>,
ref,
) => {
const { onSearch, searchPayload } = useSearch<Value>({
$iLike: searchFields as TFilterStringFields<unknown>,
$or: (searchFields as TFilterStringFields<unknown>) || [idKey, labelKey],
});
const [initialValue] = useState(value);
const { data: defaultData, isLoading: isDefaultLoading } = useFind(
const idRef = useRef(generateId());
const params = {
where: {
or: [
...(searchPayload.where.or || []),
...(value
? Array.isArray(value)
? value.map((v) => ({ [idKey]: v }))
: [{ [idKey]: value }]
: []),
],
},
};
const { data, isFetching, fetchNextPage } = useInfiniteFind(
{ entity, format },
{
params: {
where: {
...(initialValue && Array.isArray(initialValue)
? initialValue.length > 1
? { or: initialValue.map((v) => ({ [idKey]: v })) }
: { [idKey]: initialValue[0] }
: { [idKey]: initialValue }),
},
},
params,
hasCount: false,
},
{
keepPreviousData: true,
enabled: Array.isArray(initialValue) ? initialValue.length > 0 : !!value,
queryKey: [QueryType.collection, entity, `autocomplete/${idRef.current}`],
},
);
const { data: newData, isLoading } = useFind(
{ entity, format },
{
params: {
...searchPayload,
where: { ...searchPayload.where, skip: 0, limit: 10 },
},
hasCount: false,
},
{
keepPreviousData: true,
},
);
const data = [...(defaultData || []), ...(newData || [])];
const [accumulatedOptions, setAccumulatedOptions] = useState<
Map<string, Value>
>(new Map());
// 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(() => {
if (data) {
const newOptions =
preprocess && data
? preprocess((data || []) as Value[])
: ((data || []) as Value[]);
setAccumulatedOptions((prevMap) => {
const newMap = new Map(prevMap);
newOptions.forEach((option) => {
const id = option[idKey];
if (!newMap.has(id)) {
newMap.set(id, option);
}
});
return newMap;
});
}
}, [JSON.stringify(newData), idKey]);
const options = Array.from(accumulatedOptions.values());
fetchNextPage({ pageParam: params });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(searchPayload)]);
return (
<AutoCompleteSelect<Value, Label, Multiple>
@ -140,11 +119,12 @@ const AutoCompleteEntitySelect = <
multiple={multiple}
ref={ref}
idKey={idKey}
labelKey={labelKey}
options={options || []}
onSearch={onSearch}
error={error}
helperText={helperText}
loading={isLoading || isDefaultLoading}
loading={isFetching}
{...rest}
/>
);

View File

@ -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 { Box, Chip, ChipTypeMap } from "@mui/material";
import { Box, Chip, ChipTypeMap, CircularProgress } from "@mui/material";
import Autocomplete, {
AutocompleteProps,
AutocompleteValue,
@ -185,6 +185,9 @@ const AutoCompleteSelect = <
{options.length === 0 && !loading && noOptionsWarning && (
<AlertAdornment title={noOptionsWarning} type="warning" />
)}
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{props.InputProps.endAdornment}
</>
),

View File

@ -0,0 +1,90 @@
/*
* 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 { useInfiniteQuery, UseInfiniteQueryOptions } from "react-query";
import {
EntityType,
Format,
QueryType,
TPopulateTypeFromFormat,
} from "@/services/types";
import {
IBaseSchema,
IDynamicProps,
IFindConfigProps,
POPULATE_BY_TYPE,
TAllowedFormat,
TType,
} from "@/types/base.types";
import { useNormalizeAndCache } from "./helpers";
import { useGetFromCache } from "./useGet";
import { useEntityApiClient } from "../useApiClient";
export const useInfiniteFind = <
TDynamicProps extends IDynamicProps,
TAttr = TType<TDynamicProps["entity"]>["attributes"],
TBasic extends IBaseSchema = TType<TDynamicProps["entity"]>["basic"],
TFull extends IBaseSchema = TType<TDynamicProps["entity"]>["full"],
P = TPopulateTypeFromFormat<TDynamicProps>,
>(
{ entity, format }: TDynamicProps & TAllowedFormat<TDynamicProps["entity"]>,
config?: IFindConfigProps,
options?: Omit<
UseInfiniteQueryOptions<
string[],
Error,
string[],
[QueryType, EntityType, any]
>,
"queryFn" | "onSuccess"
> & { onSuccess?: (result: TBasic[]) => void },
) => {
const { onSuccess, queryKey, ...otherOptions } = options || {};
const api = useEntityApiClient<TAttr, TBasic, TFull>(entity);
const normalizeAndCache = useNormalizeAndCache<TBasic | TFull, string[]>(
entity,
);
const getFromCache = useGetFromCache(entity);
// @TODO : fix the following
// @ts-ignore
const { data: infiniteData, ...infiniteQuery } = useInfiniteQuery({
queryKey,
queryFn: async () => {
const data = await api.find(
{
...(config?.params || {}),
},
format === Format.FULL && (POPULATE_BY_TYPE[entity] as P),
);
const { entities, result } = normalizeAndCache(data);
if (onSuccess) {
onSuccess(
Object.values(entities[entity] as unknown as Record<string, TBasic>),
);
}
return result;
},
...(otherOptions || {}),
});
return {
...infiniteQuery,
data: infiniteData
? {
...infiniteData,
pages: (infiniteData?.pages || []).map((page) =>
page.map((id) => getFromCache(id) as unknown as TBasic),
),
}
: undefined,
};
};