Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix duplicate autocomplete suggestions. #991

Merged
merged 1 commit into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend-project/src/components/FetchLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<AutocompleteServiceType[keyof AutocompleteServiceType]>>[number] & {
name?: string;
Expand Down
19 changes: 18 additions & 1 deletion frontend-project/src/components/FetchSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Mode extends 'tags' ? string : number>,
Expand Down Expand Up @@ -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 (
Expand All @@ -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([]);
Expand All @@ -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 (
<Select<OptionsType>
showSearch
Expand All @@ -101,7 +118,7 @@ export function FetchSelect<
onSearch={debounceFetcher}
notFoundContent={isFetching ? <Spin size="small" /> : null}
{...props}
options={[...shownOptions, ...autocompleteOptions]}
options={options}
value={value}
/>
);
Expand Down