diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index eea5e85b4..f57f186be 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useRef, ChangeEvent, KeyboardEvent } from 'react'; import { Icon } from 'UI'; import APIClient from 'App/api_client'; import { debounce } from 'App/utils'; @@ -135,168 +135,177 @@ interface Props { hideOrText?: boolean; } -function FilterAutoComplete(props: Props) { - const { - showCloseButton = false, - placeholder = 'Type to search', - method = 'GET', - showOrButton = false, - onRemoveValue = () => null, - onAddValue = () => null, - endpoint = '', - params = {}, - value = '', - hideOrText = false - } = props; - +const FilterAutoComplete: React.FC = ({ + showCloseButton = false, + placeholder = 'Type to search', + method = 'GET', + showOrButton = false, + endpoint = '', + params = {}, + value = '', + hideOrText = false, + onSelect, + onRemoveValue, + onAddValue + }: Props) => { const [loading, setLoading] = useState(false); - const [options, setOptions] = useState([]); + const [options, setOptions] = useState<{ value: string; label: string }[]>([]); const [query, setQuery] = useState(value); const [menuIsOpen, setMenuIsOpen] = useState(false); const [initialFocus, setInitialFocus] = useState(false); - let selectRef: any = null; - let inputRef: any = null; + const [previousQuery, setPreviousQuery] = useState(value); + const selectRef = useRef(null); + const inputRef = useRef(null); const { filterStore } = useStore(); const _params = processKey(params); - const [topValues, setTopValues] = useState([]); + const filterKey = `${_params.type}${_params.key || ''}`; + const topValues = filterStore.topValues[filterKey] || []; const [topValuesLoading, setTopValuesLoading] = useState(false); - useEffect(() => { - const fetchValues = async () => { - setTopValuesLoading(true); - const values = await filterStore.getTopValues(_params.type); - setTopValues(values); - + const loadTopValues = () => { + setTopValuesLoading(true); + filterStore.fetchTopValues(_params.type, _params.key).finally(() => { setTopValuesLoading(false); setLoading(false); - }; - fetchValues().then(r => { }); - }, []); + }; + + useEffect(() => { + if (topValues.length > 0) { + const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value })); + setOptions(mappedValues); + if (!query.length && initialFocus) { + setMenuIsOpen(true); + } + } + }, [topValues, initialFocus, query.length]); + + useEffect(loadTopValues, [_params.type]); useEffect(() => { setQuery(value); }, [value]); - const loadOptions = (inputValue: string, callback: (options: []) => void) => { + const loadOptions = async (inputValue: string, callback: (options: { value: string; label: string }[]) => void) => { if (!inputValue.length) { - setOptions(topValues.map((i: any) => ({ value: i.value, label: i.value }))); - callback(topValues.map((i: any) => ({ value: i.value, label: i.value }))); + const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value })); + setOptions(mappedValues); + callback(mappedValues); setLoading(false); return; } - // @ts-ignore - new APIClient() - [method?.toLocaleLowerCase()](endpoint, { ..._params, q: inputValue }) - .then((response: any) => { - return response.json(); - }) - .then(({ data }: any) => { - const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || []; - setOptions(_options); - callback(_options); - setLoading(false); - }) - .catch((e) => { - throw new Error(e); - }); + try { + const response = await new APIClient()[method.toLowerCase()](endpoint, { ..._params, q: inputValue }); + const data = await response.json(); + const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || []; + setOptions(_options); + callback(_options); + } catch (e) { + throw new Error(e); + } finally { + setLoading(false); + } }; - const debouncedLoadOptions = React.useCallback(debounce(loadOptions, 1000), [params, topValues]); + const debouncedLoadOptions = useCallback(debounce(loadOptions, 1000), [params, topValues]); const handleInputChange = (newValue: string) => { setLoading(true); setInitialFocus(true); setQuery(newValue); - debouncedLoadOptions(newValue, (opt: any) => { - selectRef?.focus(); + debouncedLoadOptions(newValue, () => { + selectRef.current?.focus(); }); }; - const onChange = (item: any) => { + const handleChange = (item: { value: string }) => { setMenuIsOpen(false); - setQuery(item); - props.onSelect(null, item); + setQuery(item.value); + onSelect(null, item.value); }; - const onFocus = () => { + const handleFocus = () => { setInitialFocus(true); if (!query.length) { setLoading(topValuesLoading); setMenuIsOpen(!topValuesLoading && topValues.length > 0); - setOptions(topValues.map((i: any) => ({ value: i.value, label: i.value }))); + setOptions(topValues.map((i) => ({ value: i.value, label: i.value }))); } else { setMenuIsOpen(true); } }; - const onBlur = () => { + const handleBlur = () => { setMenuIsOpen(false); - props.onSelect(null, query); + setInitialFocus(false); + if (query !== previousQuery) { + onSelect(null, query); + } + setPreviousQuery(query); }; - const selected = value ? options.find((i: any) => i.value === query) : null; - const uniqueOptions = options.filter((i: Record) => i.value !== query); + const selected = value ? options.find((i) => i.value === query) : null; + const uniqueOptions = options.filter((i) => i.value !== query); const selectOptionsArr = query.length ? [{ value: query, label: query }, ...uniqueOptions] : options; return (
(inputRef = ref)} + ref={inputRef} className="w-full rounded px-2 no-focus" value={query} - onChange={({ target: { value } }: any) => handleInputChange(value)} - onClick={onFocus} - onFocus={onFocus} - onBlur={onBlur} + onChange={(e: ChangeEvent) => handleInputChange(e.target.value)} + onClick={handleFocus} + onFocus={handleFocus} + onBlur={handleBlur} placeholder={placeholder} - onKeyDown={(e: any) => { + onKeyDown={(e: KeyboardEvent) => { if (e.key === 'Enter') { - inputRef?.blur(); + inputRef.current.blur(); } }} /> {loading && ( -
+
)}