ra-core
Version:
Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React
285 lines (268 loc) • 8.95 kB
text/typescript
import { useCallback, isValidElement, ReactElement } from 'react';
import set from 'lodash/set';
import {
useChoices,
OptionText,
UseChoicesOptions,
} from './choices/useChoices';
import { useTranslate } from '../i18n';
/*
* Returns helper functions for suggestions handling.
*
* @param allowDuplicates A boolean indicating whether a suggestion can be added several times
* @param choices An array of available choices
* @param limitChoicesToValue A boolean indicating whether the initial suggestions should be limited to the currently selected one(s)
* @param matchSuggestion Optional unless `optionText` is a React element. Function which check whether a choice matches a filter. Must return a boolean.
* @param optionText Either a string defining the property to use to get the choice text, a function or a React element
* @param optionValue The property to use to get the choice value
* @param selectedItem The currently selected item. Maybe an array of selected items
* @param suggestionLimit The maximum number of suggestions returned
* @param translateChoice A boolean indicating whether to option text should be translated
*
* @returns An object with helper functions:
* - getChoiceText: Returns the choice text or a React element
* - getChoiceValue: Returns the choice value
* - getSuggestions: A function taking a filter value (string) and returning the matching suggestions
*/
export const useSuggestions = ({
allowCreate,
choices,
createText = 'ra.action.create',
createValue = '@@create',
createHintValue = '@@ra-create-hint',
limitChoicesToValue,
matchSuggestion,
optionText,
optionValue,
selectedItem,
suggestionLimit = 0,
translateChoice,
}: UseSuggestionsOptions) => {
const translate = useTranslate();
const { getChoiceText, getChoiceValue } = useChoices({
optionText,
optionValue,
translateChoice,
createValue,
createHintValue,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
const getSuggestions = useCallback(
getSuggestionsFactory({
allowCreate,
choices,
createText,
createValue,
getChoiceText,
getChoiceValue,
limitChoicesToValue,
matchSuggestion,
optionText,
optionValue,
selectedItem,
suggestionLimit,
}),
[
allowCreate,
choices,
createText,
createValue,
getChoiceText,
getChoiceValue,
limitChoicesToValue,
matchSuggestion,
optionText,
optionValue,
selectedItem,
suggestionLimit,
translate,
]
);
return {
getChoiceText,
getChoiceValue,
getSuggestions,
};
};
const escapeRegExp = value =>
value ? value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : ''; // $& means the whole matched string
export interface UseSuggestionsOptions extends UseChoicesOptions {
allowCreate?: boolean;
allowDuplicates?: boolean;
choices?: any[];
createText?: string;
limitChoicesToValue?: boolean;
matchSuggestion?: (
filter: string,
suggestion: any,
exact?: boolean
) => boolean;
suggestionLimit?: number;
selectedItem?: any | any[];
}
/**
* Default matcher implementation which check whether the suggestion text matches the filter.
*/
const defaultMatchSuggestion =
getChoiceText =>
(filter, suggestion, exact = false) => {
const suggestionText = getChoiceText(suggestion);
const isReactElement = isValidElement(suggestionText);
const regex = escapeRegExp(filter);
return isReactElement
? false
: suggestionText &&
!!suggestionText.match(
// We must escape any RegExp reserved characters to avoid errors
// For example, the filter might contain * which must be escaped as \*
new RegExp(exact ? `^${regex}$` : regex, 'i')
);
};
/**
* 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],
* })('pub')
*
* // 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 =
({
allowCreate = false,
choices = [],
createText = 'ra.action.create',
createValue = '@@create',
optionText = 'name',
optionValue = 'id',
getChoiceText,
getChoiceValue,
limitChoicesToValue = false,
matchSuggestion = defaultMatchSuggestion(getChoiceText),
selectedItem,
suggestionLimit = 0,
}: UseSuggestionsOptions & {
getChoiceText: (choice: any) => string | ReactElement;
getChoiceValue: (choice: any) => string;
}) =>
filter => {
let suggestions: any[] = [];
// if an item is selected and matches the filter
if (
selectedItem &&
!Array.isArray(selectedItem) &&
matchSuggestion(filter, selectedItem)
) {
if (limitChoicesToValue) {
// display only the selected item
suggestions = choices.filter(
choice =>
getChoiceValue(choice) === getChoiceValue(selectedItem)
);
} else {
suggestions = [...choices];
}
} else {
suggestions = choices.filter(
choice =>
matchSuggestion(filter, choice) ||
(selectedItem != null &&
(!Array.isArray(selectedItem)
? getChoiceValue(choice) ===
getChoiceValue(selectedItem)
: selectedItem.some(
selected =>
getChoiceValue(choice) ===
getChoiceValue(selected)
)))
);
}
suggestions = limitSuggestions(suggestions, suggestionLimit);
const hasExactMatch = suggestions.some(suggestion =>
matchSuggestion(filter, suggestion, true)
);
if (allowCreate) {
const filterIsSelectedItem =
// If the selectedItem is an array (for example AutocompleteArrayInput)
// we shouldn't try to match
!!selectedItem && !Array.isArray(selectedItem)
? matchSuggestion(filter, selectedItem, true)
: false;
if (!hasExactMatch && !filterIsSelectedItem) {
suggestions.push(
getSuggestion({
optionText,
optionValue,
text: createText,
value: createValue,
})
);
}
}
// Only keep unique items. Necessary because we might have fetched
// the currently selected choice in addition of the possible choices
// that may also contain it
const result = suggestions.filter(
(suggestion, index) => suggestions.indexOf(suggestion) === index
);
return result;
};
/**
* @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;
/**
* addSuggestion(
* [{ 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
* @param options.optionText
*/
const getSuggestion = ({
optionText = 'name',
optionValue = 'id',
text = '',
value = null,
}: {
optionText: OptionText;
optionValue: string;
text: string;
value: any;
}) => {
const suggestion = {};
set(suggestion, optionValue, value);
if (typeof optionText === 'string') {
set(suggestion, optionText, text);
}
return suggestion;
};