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(