UNPKG

@aibsweb/faceted-search

Version:
462 lines (378 loc) 18.3 kB
"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' };