From 74fda2721054a8f6777117c0fbdfe1087d619fcf Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 8 Nov 2019 07:40:49 +0100 Subject: [PATCH 1/3] Add empty value at the beginning rather than the end --- packages/ra-core/src/form/useSuggestions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/form/useSuggestions.ts b/packages/ra-core/src/form/useSuggestions.ts index af0aab14b8c..ec86e9eda70 100644 --- a/packages/ra-core/src/form/useSuggestions.ts +++ b/packages/ra-core/src/form/useSuggestions.ts @@ -185,7 +185,7 @@ export const getSuggestionsFactory = ({ if (typeof optionText !== 'function') { set(emptySuggestion, optionText, emptyText); } - return finalChoices.concat(emptySuggestion); + return [].concat(emptySuggestion, finalChoices); } return finalChoices; From 7bb730e5668afdf76c9dd1012d7ff62699bd1cbe Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 8 Nov 2019 08:49:58 +0100 Subject: [PATCH 2/3] make useSuggestions readable and fix bugs --- packages/ra-core/src/form/useSuggestions.ts | 160 ++++++++++++------ .../src/input/AutocompleteInput.tsx | 3 +- 2 files changed, 111 insertions(+), 52 deletions(-) diff --git a/packages/ra-core/src/form/useSuggestions.ts b/packages/ra-core/src/form/useSuggestions.ts index ec86e9eda70..c642fbe0586 100644 --- a/packages/ra-core/src/form/useSuggestions.ts +++ b/packages/ra-core/src/form/useSuggestions.ts @@ -118,14 +118,24 @@ 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', - * optionValue: 'id', - * getSuggestionText: choice => choice[optionText], + * choices: [{ id: 1, name: 'admin' }, { id: 2, name: 'publisher' }], + * optionText: 'name', + * optionValue: 'id', + * getSuggestionText: choice => choice[optionText], * })('pub') * - * Will return [{ id: 2, name: 'publisher' }] + * // 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' }] + */ export const getSuggestionsFactory = ({ choices = [], @@ -136,79 +146,127 @@ export const getSuggestionsFactory = ({ optionValue, getChoiceText, getChoiceValue, - limitChoicesToValue, + limitChoicesToValue = false, matchSuggestion = defaultMatchSuggestion(getChoiceText), selectedItem, suggestionLimit = 0, }) => filter => { - // When we display the suggestions for the first time and the input - // already has a value, we want to display more choices than just the - // currently selected one, unless limitChoicesToValue was set to true + let suggestions = []; + // if an item is selected and matches the filter if ( selectedItem && !Array.isArray(selectedItem) && matchSuggestion(filter, selectedItem) ) { if (limitChoicesToValue) { - return limitSuggestions( - choices.filter( - choice => - getChoiceValue(choice) === getChoiceValue(selectedItem) - ), - suggestionLimit + // display only the selected item + suggestions = choices.filter( + choice => + getChoiceValue(choice) === getChoiceValue(selectedItem) + ); + } else { + // ignore the filter to show more choices + suggestions = removeAlreadySelectedSuggestions( + choices, + selectedItem, + getChoiceValue ); } - - return limitSuggestions( - removeAlreadySelectedSuggestions(selectedItem, getChoiceValue)( - choices - ), - suggestionLimit + } else { + suggestions = choices.filter(choice => matchSuggestion(filter, choice)); + suggestions = removeAlreadySelectedSuggestions( + suggestions, + selectedItem, + getChoiceValue ); } - const filteredChoices = choices.filter(choice => - matchSuggestion(filter, choice) - ); - - const finalChoices = limitSuggestions( - removeAlreadySelectedSuggestions(selectedItem, getChoiceValue)( - filteredChoices - ), - suggestionLimit - ); + suggestions = limitSuggestions(suggestions, suggestionLimit); if (allowEmpty) { - const emptySuggestion = {}; - set(emptySuggestion, optionValue, emptyValue); - - if (typeof optionText !== 'function') { - set(emptySuggestion, optionText, emptyText); - } - return [].concat(emptySuggestion, finalChoices); + suggestions = addEmptySuggestion(suggestions, { + optionText, + optionValue, + emptyText, + emptyValue, + }); } - return finalChoices; + return suggestions; }; +/** + * @example + * + * removeAlreadySelectedSuggestions( + * [{ id: 1, name: 'foo'}, { id: 2, name: 'bar' }], + * [{ id: 1, name: 'foo'}] + * ); + * + * // Will return [{ id: 2, name: 'bar' }] + * + * @param suggestions List of suggestions + * @param selectedItems List of selection + * @param getChoiceValue Converter function fro msuggestion to value + */ const removeAlreadySelectedSuggestions = ( - selectedItem, - getChoiceValue -) => suggestions => { - if (!Array.isArray(selectedItem)) { - return suggestions; - } - - const selectedValues = selectedItem.map(getChoiceValue); + suggestions: any[], + selectedItems: any[] | any, + getChoiceValue: (suggestion: any) => any +) => { + const selectedValues = Array.isArray(selectedItems) + ? selectedItems.map(getChoiceValue) + : [getChoiceValue(selectedItems)]; return suggestions.filter( suggestion => !selectedValues.includes(getChoiceValue(suggestion)) ); }; -const limitSuggestions = (suggestions, limit = 0) => { - if (Number.isInteger(limit) && limit > 0) { - return suggestions.slice(0, limit); +/** + * @example + * + * limitSuggestions( + * [{ id: 1, name: 'foo'}, { id: 2, name: 'bar' }], + * 1 + * ); + * + * // Will return [{ id: 1, name: 'foo' }] + * + * @param suggestions List of suggestions + * @param limit + */ +const limitSuggestions = (suggestions: any[], limit: any = 0) => + Number.isInteger(limit) && limit > 0 + ? suggestions.slice(0, limit) + : suggestions; + +/** + * addEmptySuggestion( + * [{ id: 1, name: 'foo'}, { id: 2, name: 'bar' }], + * ); + * + * // Will return [{ id: null, name: '' }, { id: 1, name: 'foo' }, , { id: 2, name: 'bar' }] + * + * @param suggestions List of suggestions + * @param options + */ +const addEmptySuggestion = ( + suggestions: any[], + { + optionText = 'name', + optionValue = 'id', + emptyText = '', + emptyValue = null, } - return suggestions; +) => { + let newSuggestions = suggestions; + + const emptySuggestion = {}; + set(emptySuggestion, optionValue, emptyValue); + if (typeof optionText === 'string') { + set(emptySuggestion, optionText, emptyText); + } + + return [].concat(emptySuggestion, newSuggestions); }; diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx index 152f48b4bba..3d1cb4a9fff 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, + useState, FunctionComponent, useMemo, isValidElement, @@ -165,7 +166,7 @@ const AutocompleteInput: FunctionComponent< ...rest, }); - const [filterValue, setFilterValue] = React.useState(''); + const [filterValue, setFilterValue] = useState(''); const getSuggestionFromValue = useCallback( value => choices.find(choice => get(choice, optionValue) === value), From 322e3a70572c3d4188e3d23bb89bc4a0f5f2d5dc Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 8 Nov 2019 13:37:37 +0100 Subject: [PATCH 3/3] Fix unit tests --- packages/ra-core/src/form/useSuggestions.spec.ts | 8 ++++---- packages/ra-core/src/form/useSuggestions.ts | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/ra-core/src/form/useSuggestions.spec.ts b/packages/ra-core/src/form/useSuggestions.spec.ts index c916a3be19c..b0211289358 100644 --- a/packages/ra-core/src/form/useSuggestions.spec.ts +++ b/packages/ra-core/src/form/useSuggestions.spec.ts @@ -61,8 +61,8 @@ describe('getSuggestions', () => { ...defaultOptions, limitChoicesToValue: false, selectedItem: choices[0], - })('one') - ).toEqual(choices); + })('o') // should not filter 'two' + ).toEqual([{ id: 2, value: 'two' }, { id: 3, value: 'three' }]); }); it('should filter choices according to the currently selected value if selectedItem is not an array and limitChoicesToValue is true', () => { @@ -81,10 +81,10 @@ describe('getSuggestions', () => { allowEmpty: true, })('') ).toEqual([ + { id: null, value: '' }, { id: 1, value: 'one' }, { id: 2, value: 'two' }, { id: 3, value: 'three' }, - { id: null, value: '' }, ]); }); @@ -103,9 +103,9 @@ describe('getSuggestions', () => { allowEmpty: true, })('') ).toEqual([ + { id: null, value: '' }, { id: 1, value: 'one' }, { id: 2, value: 'two' }, - { id: null, value: '' }, ]); }); }); diff --git a/packages/ra-core/src/form/useSuggestions.ts b/packages/ra-core/src/form/useSuggestions.ts index c642fbe0586..8029593f2c5 100644 --- a/packages/ra-core/src/form/useSuggestions.ts +++ b/packages/ra-core/src/form/useSuggestions.ts @@ -207,13 +207,16 @@ export const getSuggestionsFactory = ({ * * @param suggestions List of suggestions * @param selectedItems List of selection - * @param getChoiceValue Converter function fro msuggestion to value + * @param getChoiceValue Converter function from suggestion to value */ const removeAlreadySelectedSuggestions = ( suggestions: any[], selectedItems: any[] | any, getChoiceValue: (suggestion: any) => any ) => { + if (!selectedItems) { + return suggestions; + } const selectedValues = Array.isArray(selectedItems) ? selectedItems.map(getChoiceValue) : [getChoiceValue(selectedItems)];