optimizely-oui
Version:
Optimizely's Component Library.
203 lines (187 loc) • 7.9 kB
JavaScript
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;