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] Fix autocompleteinput allowempty #3953

Merged
merged 3 commits into from
Nov 8, 2019
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
8 changes: 4 additions & 4 deletions packages/ra-core/src/form/useSuggestions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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: '' },
]);
});

Expand All @@ -103,9 +103,9 @@ describe('getSuggestions', () => {
allowEmpty: true,
})('')
).toEqual([
{ id: null, value: '' },
{ id: 1, value: 'one' },
{ id: 2, value: 'two' },
{ id: null, value: '' },
]);
});
});
159 changes: 110 additions & 49 deletions packages/ra-core/src/form/useSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [],
Expand All @@ -136,79 +146,130 @@ 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 finalChoices.concat(emptySuggestion);
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 from suggestion to value
*/
const removeAlreadySelectedSuggestions = (
selectedItem,
getChoiceValue
) => suggestions => {
if (!Array.isArray(selectedItem)) {
suggestions: any[],
selectedItems: any[] | any,
getChoiceValue: (suggestion: any) => any
) => {
if (!selectedItems) {
return suggestions;
}

const selectedValues = selectedItem.map(getChoiceValue);
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);
};
3 changes: 2 additions & 1 deletion packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {
useCallback,
useEffect,
useRef,
useState,
FunctionComponent,
useMemo,
isValidElement,
Expand Down Expand Up @@ -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),
Expand Down