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

[RFR] Add allowDuplicates for AutocompleteArrayInput #4026

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
4 changes: 3 additions & 1 deletion docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,10 @@ If you need to override the props of the suggestions container (a `Popper` eleme

| Prop | Required | Type | Default | Description |
| ---|---|---|---|--- |
| `allowEmpty` | Optional | `boolean` | `false` | If `true`, the first option is an empty one |
| `allowDuplicates` | Optional | `boolean` | `false` | If `true`, the options can be selected several times |
| `choices` | Required | `Object[]` | - | List of items to autosuggest |
| `matchSuggestion` | Optional | `Function` | - | Required if `optionText` is a React element. Function returning a boolean indicating whether a choice matches the filter. `(filter, choice) => boolean`
| `matchSuggestion` | Optional | `Function` | - | Required if `optionText` is a React element. Function returning a boolean indicating whether a choice matches the filter. `(filter, choice) => boolean` |
| `optionValue` | Optional | `string` | `id` | Fieldname of record containing the value to use as input value |
| `optionText` | Optional | <code>string &#124; Function</code> | `name` | Fieldname of record to display in the suggestion item or function which accepts the current record as argument (`(record)=> {string}`) |
| `setFilter` | Optional | `Function` | `null` | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically setup when using `ReferenceInput`. |
Expand Down
24 changes: 24 additions & 0 deletions packages/ra-core/src/form/useSuggestions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,28 @@ describe('getSuggestions', () => {
expect(getSuggestions(defaultOptions)(false)).toEqual(choices);
expect(getSuggestions(defaultOptions)(null)).toEqual(choices);
});

it('should return all choices if allowDuplicates is true', () => {
expect(
getSuggestions({
...defaultOptions,
allowDuplicates: true,
selectedItem: choices[0],
})('')
).toEqual([
{ id: 1, value: 'one' },
{ id: 2, value: 'two' },
{ id: 3, value: 'three' },
]);
});

it('should return all the filtered choices if allowDuplicates is true', () => {
expect(
getSuggestions({
...defaultOptions,
allowDuplicates: true,
selectedItem: [choices[0]],
})('o')
).toEqual([{ id: 1, value: 'one' }, { id: 2, value: 'two' }]);
});
});
39 changes: 24 additions & 15 deletions packages/ra-core/src/form/useSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTranslate } from '../i18n';
/*
* Returns helper functions for suggestions handling.
*
* @param allowDuplicates A boolean indicating whether a suggestion can be added several times
* @param allowEmpty A boolean indicating whether an empty suggestion should be added
* @param choices An array of available choices
* @param emptyText The text to use for the empty suggestion. Defaults to an empty string
Expand All @@ -24,6 +25,7 @@ import { useTranslate } from '../i18n';
* - getSuggestions: A function taking a filter value (string) and returning the matching suggestions
*/
const useSuggestions = ({
allowDuplicates,
allowEmpty,
choices,
emptyText = '',
Expand All @@ -45,6 +47,7 @@ const useSuggestions = ({

const getSuggestions = useCallback(
getSuggestionsFactory({
allowDuplicates,
allowEmpty,
choices,
emptyText: translate(emptyText, { _: emptyText }),
Expand All @@ -59,6 +62,7 @@ const useSuggestions = ({
suggestionLimit,
}),
[
allowDuplicates,
allowEmpty,
choices,
emptyText,
Expand Down Expand Up @@ -89,6 +93,7 @@ const escapeRegExp = value =>

interface Options extends UseChoicesOptions {
choices: any[];
allowDuplicates?: boolean;
allowEmpty?: boolean;
emptyText?: string;
emptyValue?: any;
Expand Down Expand Up @@ -119,7 +124,7 @@ const defaultMatchSuggestion = getChoiceText => (filter, suggestion) => {
* Get the suggestions to display after applying a fuzzy search on the available choices
*
* @example
*
*
* getSuggestions({
* choices: [{ id: 1, name: 'admin' }, { id: 2, name: 'publisher' }],
* optionText: 'name',
Expand All @@ -129,17 +134,17 @@ const defaultMatchSuggestion = getChoiceText => (filter, suggestion) => {
*
* // Will return [{ id: 2, name: 'publisher' }]
* getSuggestions({
* choices: [{ id: 1, name: 'admin' }, { id: 2, name: 'publisher' }],
* optionText: 'name',
* optionValue: 'id',
* getSuggestionText: choice => choice[optionText],
* })('pub')
*
* // Will return [{ id: 2, name: 'publisher' }]

* choices: [{ id: 1, name: 'admin' }, { id: 2, name: 'publisher' }],
* optionText: 'name',
* optionValue: 'id',
* getSuggestionText: choice => choice[optionText],
* })('pub')
*
* // Will return [{ id: 2, name: 'publisher' }]
*/
export const getSuggestionsFactory = ({
choices = [],
allowDuplicates,
allowEmpty,
emptyText,
emptyValue,
Expand All @@ -165,21 +170,25 @@ export const getSuggestionsFactory = ({
choice =>
getChoiceValue(choice) === getChoiceValue(selectedItem)
);
} else {
} else if (!allowDuplicates) {
// ignore the filter to show more choices
suggestions = removeAlreadySelectedSuggestions(
choices,
selectedItem,
getChoiceValue
);
} else {
suggestions = choices;
}
} else {
suggestions = choices.filter(choice => matchSuggestion(filter, choice));
suggestions = removeAlreadySelectedSuggestions(
suggestions,
selectedItem,
getChoiceValue
);
if (!allowDuplicates) {
suggestions = removeAlreadySelectedSuggestions(
suggestions,
selectedItem,
getChoiceValue
);
}
}

suggestions = limitSuggestions(suggestions, suggestionLimit);
Expand Down
11 changes: 7 additions & 4 deletions packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ interface Options {
const AutocompleteArrayInput: FunctionComponent<
InputProps<TextFieldProps & Options> & DownshiftProps<any>
> = ({
allowDuplicates,
allowEmpty,
classes: classesOverride,
choices = [],
Expand Down Expand Up @@ -178,6 +179,7 @@ const AutocompleteArrayInput: FunctionComponent<
);

const { getChoiceText, getChoiceValue, getSuggestions } = useSuggestions({
allowDuplicates,
allowEmpty,
choices,
emptyText,
Expand Down Expand Up @@ -234,13 +236,14 @@ const AutocompleteArrayInput: FunctionComponent<

const handleChange = useCallback(
(item: any) => {
let newSelectedItems = selectedItems.includes(item)
? [...selectedItems]
: [...selectedItems, item];
let newSelectedItems =
!allowDuplicates && selectedItems.includes(item)
? [...selectedItems]
: [...selectedItems, item];
setFilterValue('');
input.onChange(newSelectedItems.map(getChoiceValue));
},
[getChoiceValue, input, selectedItems, setFilterValue]
[allowDuplicates, getChoiceValue, input, selectedItems, setFilterValue]
);

const handleDelete = useCallback(
Expand Down