@aibsweb/faceted-search
Version:
A generalized faceted search application.
462 lines (378 loc) • 18.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
require("@babel/polyfill");
require("custom-event-polyfill");
require("whatwg-fetch");
var _isFunction = _interopRequireDefault(require("lodash/isFunction"));
var _has = _interopRequireDefault(require("lodash/has"));
var _get = _interopRequireDefault(require("lodash/get"));
var _debounce = _interopRequireDefault(require("lodash/debounce"));
var _react = _interopRequireDefault(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _reactApollo = require("react-apollo");
var _apolloBoost = require("apollo-boost");
var _reactTabs = require("react-tabs");
require("../../scss/layout/stacked.scss");
require("../../scss/faceted-search.scss");
require("react-tabs/style/react-tabs.scss");
var _stateStore = _interopRequireDefault(require("../classes/state-store"));
var _componentDefinitionValidationHelper = _interopRequireDefault(require("../helpers/component-definition-validation-helper"));
var _enums = require("../enums");
var _loadIndicator = _interopRequireDefault(require("./status/load-indicator"));
var _errorIndicator = _interopRequireDefault(require("./status/error-indicator"));
var _filterBox = _interopRequireDefault(require("./filter-box/filter-box"));
var _countsRatioBox = _interopRequireDefault(require("./counts-ratio-box/counts-ratio-box"));
var _dataTable = _interopRequireDefault(require("./data-table/data-table"));
var _pivotTable = _interopRequireDefault(require("./pivot-table"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
var FacetedSearch =
/*#__PURE__*/
function (_React$Component) {
_inherits(FacetedSearch, _React$Component);
/***
* The faceted search component is the entry point for the facetedSearch application. This component is responsible for loading
* and rendering the components provided in the config.
*
* @param {Object} props
* @param {json} props.config - Application configuration containing props for each component.
* @param {string} props.configUrl - The application config url when the config is hosted elsewhere.
* @param {Object} props.componentDefinitions - Contains a query builder, a data transform function, and layout position property
* @param {string} props.layout - The name of the layout scss to use, defaults to 'stacked'
*/
function FacetedSearch(props) {
var _this;
_classCallCheck(this, FacetedSearch);
_this = _possibleConstructorReturn(this, _getPrototypeOf(FacetedSearch).call(this, props));
_this.state = {
readyState: _enums.LOAD_STATE.INITIALIZATION,
config: props.config,
store: {}
}; // map component type names to render methods of those components
_this.componentRendererMap = {
'counts-ratio-box': _this.renderCountsRatioBox.bind(_assertThisInitialized(_this)),
'filter-box': _this.renderFilterBox.bind(_assertThisInitialized(_this)),
'data-table': _this.renderDataTable.bind(_assertThisInitialized(_this)),
'pivot-table': _this.renderPivotTable.bind(_assertThisInitialized(_this))
}; // The max rate at which scroll events should be handled (in ms).
var minTimeBetweenScrollEvents = 10; // Watch the window for scroll events and provide them to the state store
document.onscroll = (0, _debounce["default"])(_this.handleApplicationScrollDebounce.bind(_assertThisInitialized(_this)), minTimeBetweenScrollEvents);
return _this;
}
/**
* When the document is scrolled this fires an event to update the state store.
* This is debounced so as not to update too often and cause performance issues
*
* @param {*} e Scroll event
*/
_createClass(FacetedSearch, [{
key: "handleApplicationScrollDebounce",
value: function handleApplicationScrollDebounce(e) {
var scrollTop = (0, _get["default"])(e, ['target', 'scrollingElement', 'scrollTop']);
var event = new CustomEvent(_enums.EVENT_KEYS.SCROLL, {
detail: {
scrollTop: scrollTop
}
});
document.dispatchEvent(event);
}
/*eslint brace-style: ["warn", "stroustrup"]*/
}, {
key: "componentDidMount",
value: function componentDidMount() {
var _this2 = this;
var componentDefinitionsAreNotValid = !_componentDefinitionValidationHelper["default"].validateDefinitions(this.props.componentDefinitions);
var shouldFetchConfig = this.state.readyState === _enums.LOAD_STATE.INITIALIZATION && this.props.configUrl && !this.state.config;
var configCannotBeFound = !this.state.config && !this.props.configUrl; // check the component definition
if (componentDefinitionsAreNotValid) {
console.error('Provided component definitions are missing one or more properties.');
this.setState({
readyState: _enums.LOAD_STATE.ERROR
});
} // check if config should be fetched from a url
else if (shouldFetchConfig) {
this.setState({
readyState: _enums.LOAD_STATE.FETCHING
}, function () {
return fetch(_this2.props.configUrl, {
credentials: 'same-origin'
}).then(function (response) {
return response.json();
}).then(function (config) {
_this2.setState({
config: config
}, _this2.initialize);
})["catch"](function (e) {
console.error('Error while fetching config.', e.message);
_this2.setState({
readyState: _enums.LOAD_STATE.ERROR
});
});
});
} // check if both a config is not provided and there is no url to get a config
else if (configCannotBeFound) {
this.setState({
readyState: _enums.LOAD_STATE.ERROR
});
} // config was provided
else {
this.initialize();
}
}
/*eslint brace-style: ["warn", "stroustrup"]*/
/**
* Set up:
* Apollo Client;
* props for components;
* State Store;
*/
}, {
key: "initialize",
value: function initialize() {
// Set up Apollo Client
// We have no unique ids for most data
this.client = new _apolloBoost.ApolloClient({
cache: new _apolloBoost.InMemoryCache({
dataIdFromObject: function dataIdFromObject() {
return null;
}
}),
link: new _apolloBoost.HttpLink({
uri: this.state.config.network.uri
})
}); // Set up State Store
var stateStoreConfig = this.state.config['state-store'] || {};
var _ref = new _stateStore["default"](this.updateStore.bind(this), stateStoreConfig),
store = _ref.store; // app is ready to render data.
this.setState({
readyState: _enums.LOAD_STATE.READY,
store: store
});
}
/**
* A callback used to update component store. Called by the StateStore, iterate the store version
* so that deeply nested changes are reflected in components.
*
* @param {object[]} store the state store.
*/
}, {
key: "updateStore",
value: function updateStore(store) {
this.setState({
store: store
});
}
}, {
key: "render",
value: function render() {
var readyState = this.state.readyState;
if (readyState === _enums.LOAD_STATE.INITIALIZATION || readyState === _enums.LOAD_STATE.FETCHING) {
return _loadIndicator["default"];
}
if (readyState === _enums.LOAD_STATE.READY) {
return _react["default"].createElement("div", {
className: "faceted-search__container layout-".concat(this.props.layout)
}, _react["default"].createElement(_reactApollo.ApolloProvider, {
client: this.client
}, this.renderApplication()));
} // default
return _errorIndicator["default"];
}
/**
* Render an application with tabs OR render individual components
*/
}, {
key: "renderApplication",
value: function renderApplication() {
if ((0, _has["default"])(this.state.config, 'tabs')) {
return this.renderTabs(this.state.config.tabs);
} else if ((0, _has["default"])(this.state.config, 'componentProps')) {
return this.state.config.componentProps.map(this.renderComponent.bind(this));
} else {
console.error('The Faceted Search configuration requires either a list of component props or a list of tabs with component props.');
return null;
}
}
/**
* Render the tab list with labels
* Render the components belonging to each tab panel
* @param {Object} tabProps - from config
*/
}, {
key: "renderTabs",
value: function renderTabs(tabProps) {
var _this3 = this;
return _react["default"].createElement(_reactTabs.Tabs, {
className: "faceted-search__tabs",
defaultIndex: 1
}, _react["default"].createElement(_reactTabs.TabList, null, tabProps.map(function (_ref2) {
var label = _ref2.label;
return _this3.renderTab(label);
})), tabProps.map(function (_ref3) {
var label = _ref3.label,
componentProps = _ref3.componentProps;
return _this3.renderTabPanel.bind(_this3)(label, componentProps);
}));
}
/**
* Renders a table panel that contains all of the component for that panel as specified in the config.
* @param {String} label - label to be shown in the tab UI
* @param {Object[]} componentProps - Array of component props from config to be rendered
*/
}, {
key: "renderTabPanel",
value: function renderTabPanel(label, componentProps) {
return _react["default"].createElement(_reactTabs.TabPanel, {
key: label
}, _react["default"].createElement("div", {
className: "faceted-search__container layout-".concat(this.props.layout)
}, componentProps.map(this.renderComponent.bind(this))));
}
/**
* Renders the tab UI element
* @param {String} label - label for tab UI
*/
}, {
key: "renderTab",
value: function renderTab(label) {
return _react["default"].createElement(_reactTabs.Tab, {
key: label
}, label);
}
/**
* Determine which faceted search sub-component to render
*
* @param {object} componentProps - props for a component: contains definition, config, component type string, and unique id
*/
}, {
key: "renderComponent",
value: function renderComponent(componentProps) {
var componentRenderer = this.componentRendererMap[componentProps.renderer];
var className = "".concat(componentProps.className, " component__").concat(componentProps.className, " layout-").concat(this.props.layout, "__").concat(componentProps.position);
if (!(0, _isFunction["default"])(componentRenderer)) {
return null;
}
return _react["default"].createElement("div", {
key: "".concat(componentProps.componentDefinition, ":").concat(componentProps.position),
className: className
}, componentRenderer(componentProps));
}
/**
* Render method for Counts Ratio Box
* @param {object} componentProps - props for a component: contains definition, config, component type string, and unique id
*/
}, {
key: "renderCountsRatioBox",
value: function renderCountsRatioBox(componentProps) {
var componentDefinition = this.props.componentDefinitions[componentProps.componentDefinition];
var _componentDefinition$ = componentDefinition.query.buildQuery(this.state.store),
variables = _componentDefinition$.variables,
baseQuery = _componentDefinition$.baseQuery;
return _react["default"].createElement(_reactApollo.Query, {
query: baseQuery,
variables: variables,
notifyOnNetworkStatusChange: true
}, function (_ref4) {
var data = _ref4.data;
var transformedData = componentDefinition.transformer.transform(data);
return _react["default"].createElement(_countsRatioBox["default"], _extends({
data: transformedData
}, componentProps));
});
}
/**
* Render method for Filter Box
* @param {object} componentProps - props for a component: contains definition, config, component type string, and unique id
*/
}, {
key: "renderFilterBox",
value: function renderFilterBox(componentProps) {
var _this4 = this;
var componentDefinition = this.props.componentDefinitions[componentProps.componentDefinition];
var _componentDefinition$2 = componentDefinition.query.buildQuery(this.state.store),
variables = _componentDefinition$2.variables,
baseQuery = _componentDefinition$2.baseQuery;
return _react["default"].createElement(_reactApollo.Query, {
query: baseQuery,
variables: variables,
notifyOnNetworkStatusChange: true
}, function (_ref5) {
var data = _ref5.data;
var transformedData = componentDefinition.transformer.transform(data);
return _react["default"].createElement(_filterBox["default"], _extends({
data: transformedData,
store: _this4.state.store
}, componentProps));
});
}
/**
* Render method for Data Table
* @param {object} componentProps - props for a component: contains definition, config, component type string, and unique id
*/
}, {
key: "renderDataTable",
value: function renderDataTable(componentProps) {
var _this5 = this;
var componentDefinition = this.props.componentDefinitions[componentProps.componentDefinition];
var _componentDefinition$3 = componentDefinition.query.buildQuery(this.state.store),
variables = _componentDefinition$3.variables,
baseQuery = _componentDefinition$3.baseQuery;
return _react["default"].createElement(_reactApollo.Query, {
query: baseQuery,
variables: variables,
notifyOnNetworkStatusChange: true
}, function (_ref6) {
var data = _ref6.data,
loading = _ref6.loading;
var transformedData = componentDefinition.transformer.transform(data);
return _react["default"].createElement(_dataTable["default"], _extends({
data: transformedData,
store: _this5.state.store,
isLoading: loading
}, componentProps));
});
}
}, {
key: "renderPivotTable",
value: function renderPivotTable(componentProps) {
var _this6 = this;
var componentDefinition = this.props.componentDefinitions[componentProps.componentDefinition]; // The component needs to mount so that it can provide it's config vals to the state store.
var _componentDefinition$4 = componentDefinition.query.buildQuery(this.state.store),
baseQuery = _componentDefinition$4.baseQuery;
return _react["default"].createElement(_reactApollo.Query, {
query: baseQuery,
notifyOnNetworkStatusChange: true
}, function (_ref7) {
var data = _ref7.data;
return _react["default"].createElement(_pivotTable["default"], _extends({
data: data,
store: _this6.state.store,
transform: componentDefinition.transformer.transform
}, componentProps));
});
}
}]);
return FacetedSearch;
}(_react["default"].Component);
exports["default"] = FacetedSearch;
FacetedSearch.propTypes = {
config: _propTypes["default"].object,
configUrl: _propTypes["default"].string,
layout: _propTypes["default"].string,
componentDefinitions: _propTypes["default"].object.isRequired
};
FacetedSearch.defaultProps = {
layout: 'stacked'
};