Skip to content

Commit

Permalink
Merge pull request #4026 from alanpoulain/autocomplete-array-input-al…
Browse files Browse the repository at this point in the history
…low-duplicates

[RFR] Add allowDuplicates for AutocompleteArrayInput
  • Loading branch information
fzaninotto authored Nov 24, 2019
2 parents 708ee6a + f41214a commit 6f9fd6a
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 20 deletions.
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

0 comments on commit 6f9fd6a

Please sign in to comment.