diff --git a/frontend-project/src/components/FetchLink.tsx b/frontend-project/src/components/FetchLink.tsx index f9e2caa8e..06ed7e292 100644 --- a/frontend-project/src/components/FetchLink.tsx +++ b/frontend-project/src/components/FetchLink.tsx @@ -9,6 +9,12 @@ import { Awaited, KeysWithValsOfType } from '../services/common'; import { ResourceWithId } from '../services/service'; import { QQ } from '../utils/QQ'; +/** + * Given an id, queries the Autocomplete API to get a relevant human-readable name. + * + * Technically speaking, the component doesn't perform any autocompletion. It communicates + * with the autocomplete API to have a single source of truth for (id => name) mappings. + */ export function FetchLink< T extends Awaited>[number] & { name?: string; diff --git a/frontend-project/src/components/FetchSelect.tsx b/frontend-project/src/components/FetchSelect.tsx index fc45aa138..85810760f 100644 --- a/frontend-project/src/components/FetchSelect.tsx +++ b/frontend-project/src/components/FetchSelect.tsx @@ -8,7 +8,14 @@ import { openNotificationWithIcon } from '../models/global'; import { AutocompleteFunctionType, AutocompleteServiceType } from '../services/autocomplete'; import { Awaited, KeysWithValsOfType, OptionType } from '../services/common'; import { QQ } from '../utils/QQ'; +import { unionBy, sortBy } from 'lodash'; +/** + * Select field with autocompletion. + * + * Whenever a user types into the search field, the component will communicate with the backend + * to show items matching the input. + */ export function FetchSelect< Mode extends 'multiple' | 'tags' | undefined, OptionsType extends OptionType, @@ -52,6 +59,10 @@ export function FetchSelect< ); } + // Call the autocomplete api once to convert field ids, fetched from the object's detail endpoint, + // into human readable names. + // Uses the autocomplete API for (id => name) mapping for consistency - subsequent calls, triggered + // by user input, will receive (id, name) pairs from the same API. useEffect(() => { const arrayValue = Array.isArray(value) ? value : [value]; if ( @@ -75,6 +86,8 @@ export function FetchSelect< .finally(() => setFetching(false)); }, []); + // Fetch (id, name) pairs matching the search string. + // Invoked on every keystroke, after a short delay. const debounceFetcher = (search: string) => { if (!search) return []; setAutocompleteOptions([]); @@ -93,6 +106,10 @@ export function FetchSelect< ); }; + // All options to display to the user. + // Sets may overlap - remove duplicates to avoid duplicate rendering issues. + const options = sortBy(unionBy(shownOptions, autocompleteOptions, 'value'), 'label'); + return ( showSearch @@ -101,7 +118,7 @@ export function FetchSelect< onSearch={debounceFetcher} notFoundContent={isFetching ? : null} {...props} - options={[...shownOptions, ...autocompleteOptions]} + options={options} value={value} /> );