diff --git a/docs/Inputs.md b/docs/Inputs.md index 8a1180bd6d9..5e42f4fb32b 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -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 | string | Function | `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`. | diff --git a/packages/ra-core/src/form/useSuggestions.spec.ts b/packages/ra-core/src/form/useSuggestions.spec.ts index 9a24ec91771..d67d7d2c8f9 100644 --- a/packages/ra-core/src/form/useSuggestions.spec.ts +++ b/packages/ra-core/src/form/useSuggestions.spec.ts @@ -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' }]); + }); }); diff --git a/packages/ra-core/src/form/useSuggestions.ts b/packages/ra-core/src/form/useSuggestions.ts index 52064f98600..791216fa229 100644 --- a/packages/ra-core/src/form/useSuggestions.ts +++ b/packages/ra-core/src/form/useSuggestions.ts @@ -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 @@ -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 = '', @@ -45,6 +47,7 @@ const useSuggestions = ({ const getSuggestions = useCallback( getSuggestionsFactory({ + allowDuplicates, allowEmpty, choices, emptyText: translate(emptyText, { _: emptyText }), @@ -59,6 +62,7 @@ const useSuggestions = ({ suggestionLimit, }), [ + allowDuplicates, allowEmpty, choices, emptyText, @@ -89,6 +93,7 @@ const escapeRegExp = value => interface Options extends UseChoicesOptions { choices: any[]; + allowDuplicates?: boolean; allowEmpty?: boolean; emptyText?: string; emptyValue?: any; @@ -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', @@ -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, @@ -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); diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx index 6276b6beeca..401207267e5 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx @@ -93,6 +93,7 @@ interface Options { const AutocompleteArrayInput: FunctionComponent< InputProps & DownshiftProps > = ({ + allowDuplicates, allowEmpty, classes: classesOverride, choices = [], @@ -178,6 +179,7 @@ const AutocompleteArrayInput: FunctionComponent< ); const { getChoiceText, getChoiceValue, getSuggestions } = useSuggestions({ + allowDuplicates, allowEmpty, choices, emptyText, @@ -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(