UNPKG

optimizely-oui

Version:

Optimizely's Component Library.

203 lines (187 loc) 7.9 kB
import React, { Component } from "react"; import ImmutablePropTypes from "react-immutable-proptypes"; import PropTypes from "prop-types"; import classNames from "classnames"; import { isFilterTermInItem } from "../../utils/filter"; import ListItem from "./FilterPickerListItem"; /** * - Using this instead of isImmutable() because OUI does not have immutable, * only react-immutable-proptypes for propType checking * * @param {Object|Immutable.Map} itemToCheck * - item to evaluate * @returns {boolean} * - very crude check to detect whether we're dealing with an Immutable Map * or a POJO */ function isItemImmutable(itemToCheck) { return typeof itemToCheck.size === "number"; } /** * Based off isItemImmutable, uses the get() for Immutable.Maps or bracket * notation for POJO * @param {Object|Immutable.Map} pojoOrMap * - POJO or Immutable.Map to get key from * @param {string} key * - top level key to retrieve from POJO or Immutable.Map * @returns {*} */ function getKeyFromPojoOrImmutableMap(pojoOrMap, key) { return isItemImmutable(pojoOrMap) ? pojoOrMap.get(key) : pojoOrMap[key]; } /** * * @param {Array} keysToSearch * - top level keys in allEntities to join and search for the filterQuery * @param {string} filterQuery * - value of search input query * @param {Function} filterFn * - filter function to execute and return from inner function * @param {boolean} isCustomFilterFn * - determines what arguments are passed to the provided filterFn * @returns {Function} * - provided an item from an iterable (Object|Immutable.Map), this function * should return a Boolean from the executed filterFn. if isCustomFilterFn * is falsy, the default filter function will be used and only passed * filterQuery and contentToSearch. Otherwise the item argument will * precede filterQuery and contentToSearch */ function filterSetup(keysToSearch, filterQuery, filterFn, isCustomFilterFn) { return function (item) { const contentToSearch = keysToSearch .map(key => { return getKeyFromPojoOrImmutableMap(item, key) || ""; }) .join(" "); // If a customFilterFn is used, item should be sent as the first argument // before filterQuery and contentToSearch. This is worthwhile since // it makes more sense for the component consumer to receive item // first in the custom filter function. If not a custom filter function, // just send filterQuery and contentToSearch since isFilterTermInItem // doesn't need the item argument. return filterFn(...(isCustomFilterFn ? [item] : []).concat([filterQuery, contentToSearch])); }; } class FilterPicker extends Component { static propTypes = { /** * Array of Objects or Immutable.List of Immutable.Maps of all entities, * including selected ones */ allEntities: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.object), ImmutablePropTypes.listOf(ImmutablePropTypes.map), ]), /** * Function that should return a child node, passed an object * as an argument with the following keys: * - availableEntities {Array|Immutable.List}: * The difference of allEntities and entities based on provided * selectedEntityIds. Additionally, filtered via the default filter * function based on the filterQuery value set in handleFilterInput, * or alternatively a provided custom filter function * - filterQuery {string}: * Value stored in component state via handleFilterInput * - handleFilterInput {Function}: * Function that can be used in an input, that sets the input value * in component state as filterQuery. This can be used for filtering * in the default or custom filter function. If a search Input will * be used with Blocklist and FilterList.ListItem, consider wrapping * Blocklist inside a div with the oui-filter-picker-list class to * remove the Input's bottom border (see story/story source for example) * - selectedEntities {Array|Immutable.List}: * Given the optionally provided selectedEntityIds, an array or * Immutable.List of entities, based on ID, from allEntities **/ children: PropTypes.func.isRequired, /** CSS class names. */ className: PropTypes.string, /** * Optional function that, if provided, can be used in place of the * standard filter function. * - Function will receive item, filterQuery, and contentToSearch arguments * from a filter iterable, the last two values may be undefined so some * sane defaults aren't a bad idea * - Should return a boolean value, if null is returned by customFilterFn, * the default filter function will be used instead * - Example of a customFilterFn that behaves similarly to the default * filter function: * <FilterPicker * customFilterFn={ (item, filterQuery = '', contentToSearch = '') => { * return ( * !item.get('archived') && * contentToSearch.includes(filterQuery); * ); * } }> */ customFilterFn: PropTypes.func, /** * Optional array of top level keys in allEntities to join and search for * the filterQuery */ keysToSearch: PropTypes.array, /** * Optional array or Immutable.List of numbers of selected entity IDs. * Do not provide if selected entities shouldn't be filtered * from availableEntities */ selectedEntityIds: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.number), ImmutablePropTypes.listOf(PropTypes.number), ]), /** * String value for the outermost container's * data-test-section attribute **/ testSection: PropTypes.string, }; static defaultProps = { allEntities: [], customFilterFn: () => null, keysToSearch: ["name", "description"], selectedEntityIds: [], testSection: "", }; state = { filterQuery: "", }; handleFilterInput = event => { this.setState({ filterQuery: event.target.value, }); }; render() { const { filterQuery } = this.state; const { allEntities, children, className, customFilterFn, keysToSearch, selectedEntityIds, testSection, ...props } = this.props; const isCustomFilterFn = customFilterFn() !== null; const filterFn = isCustomFilterFn ? customFilterFn : isFilterTermInItem; const availableEntitiesFilter = filterSetup(keysToSearch, filterQuery, filterFn, isCustomFilterFn); const availableEntities = allEntities .filter(entity => !selectedEntityIds.includes(getKeyFromPojoOrImmutableMap(entity, "id"))) .filter(availableEntitiesFilter); const selectedEntities = allEntities.filter(entity => selectedEntityIds.includes(getKeyFromPojoOrImmutableMap(entity, "id")) ); return ( <div className={classNames("oui-filter-picker", className)} data-test-section={testSection} {...props}> {children({ handleFilterInput: this.handleFilterInput.bind(this), availableEntities, filterQuery, selectedEntities, })} </div> ); } } FilterPicker.ListItem = ListItem; export default FilterPicker;