UNPKG

labo-components

Version:
393 lines (362 loc) 15.5 kB
import React from 'react'; import PropTypes from 'prop-types'; import AggregationCreator from './AggregationCreator'; import FlexModal from '../FlexModal'; import IDUtil from '../../util/IDUtil'; import ComponentUtil from '../../util/ComponentUtil'; import ReactTooltip from 'react-tooltip'; import classNames from 'classnames'; import AggregationBox from './AggregationBox'; //this component draws the aggregations (a.k.a. facets) and merely outputs //the user selections to the parent component export default class AggregationList extends React.Component { constructor(props) { super(props); this.state = { showModal: false, showModalWarning: false, //TODO merge these three states into one, since they all keep state information per aggregation box showAllModes: {} }; this.CLASS_PREFIX = "agl"; this.minToShow = 5; this.currentFacet = null; } //communicates the selected facets back to the parent component onOutput = (desiredFacets, selectedFacets) => { if(!selectedFacets) { selectedFacets = this.props.selectedFacets; } if (this.props.onOutput) { this.props.onOutput(this.constructor.name, { desiredFacets: desiredFacets, selectedFacets: selectedFacets }); } }; onComponentOutput = (componentClass, data) => { if (componentClass === "AggregationCreator" && data) { const desiredFacets = this.props.desiredFacets; desiredFacets.unshift(data); this.onOutput(desiredFacets, this.props.selectedFacets); ComponentUtil.hideModal( this, "showModal", "field_select__modal", true ); } }; /* ------------------------------------- FACET SELECTION -------------------------------- */ toggleSelectedFacet = (key, value) => { const facets = this.props.selectedFacets; if (facets) { if (facets[key]) { const index = facets[key].indexOf(value); if (index === -1) { facets[key].push(value); //add the value } else { facets[key].splice(index, 1); // remove the value if (facets[key].length === 0) { delete facets[key]; } } } else { facets[key] = [value]; } //output to the parent component this.onOutput(this.props.desiredFacets, facets); } }; /*------------------------------------- REMOVE DIALOG (TODO MAKE NICER) ----------------------------*/ showRemoveDialog = (key, index) => { this.currentFacet = key; //FIXME this part is still nasty, but for now it's necessary to prevent //toggling the header menu when clicking the "X" if (document.querySelector("#index__" + index)) { document.querySelector("#index__" + index).addEventListener( "click", function(event) { event.preventDefault(); }, { once: true } ); ComponentUtil.showModal( this, "showModalWarning", "field_select_facet__modal", true ); } }; removeAggregation = () => { //first remove the entry from the desiredFacets const desiredFacets = this.props.desiredFacets; for (let i = desiredFacets.length - 1; i >= 0; i--) { if (desiredFacets[i].field === this.currentFacet) { desiredFacets.splice(i, 1); break; } } //then throw away any selected value from the selectedFacets if (this.props.selectedFacets) { delete this.props.selectedFacets[this.currentFacet]; } ComponentUtil.hideModal( this, "showModalWarning", "field_select_facet__modal", true ); this.onOutput(desiredFacets, this.props.selectedFacets); }; //makes sure to filter out heavy facets that will freeze the screen //(in case the search term is smaller than 3 characters long) filterHeavyFacets = (desiredFacets, allowHeavyFacets) => { const lightFacets = this.props.collectionConfig.getFacetSelectionList(false).map(f => f.value); return allowHeavyFacets ? desiredFacets : desiredFacets.filter(df => lightFacets.indexOf(df.field) !== -1) }; /*------------------------------------- FUNCTION FOR GENERATING UI FRIENDLY DATA OBJECT --------------*/ //returns render friendly object based on the data supplied in the props generateUIData = () => { //will be ultimately returned containing a list of ui data per "desired aggregation" const uiData = []; const desiredFacets = this.props.desiredFacets ? this.filterHeavyFacets(this.props.desiredFacets, this.props.allowHeavyFacets) : []; //Check if all selected facets are in the desired aggregation list, if not, add doc_count 0 Object.keys(this.props.selectedFacets).forEach(field => { this.props.selectedFacets[field].forEach(facetValue => { const found = this.props.aggregations[field].find( aggr => aggr["key"] === facetValue ); if (!found) { this.props.aggregations[field].push({ key: facetValue, doc_count: 0 }); } }); }); //loop through the desired facets, available in the state desiredFacets.forEach((da, index) => { // skip the date_histogram types, as they shouldn't be converted to a list // however, they should be included in this loop in order to keep the facet // index intact if (da.type === "date_histogram") { return; } //first check if the aggregation has anything in it const isEmptyAggr = !( this.props.aggregations[da.field] && this.props.aggregations[da.field].length > 0 ); //then parse the retrieved facets/buckets let facets = []; if (!isEmptyAggr) { facets = this.props.aggregations[da.field].map(facet => { return { key: facet.key, guid: da.field + "|" + facet.key, count: facet.doc_count, selected: this.props.selectedFacets[da.field] && this.props.selectedFacets[da.field].indexOf( facet.key ) !== -1 }; }); // move the selected facets to the top of the array, so they always appear on top in the list const selectedFacets = facets.filter(f => f.selected); facets = facets.filter(f => !f.selected); facets.unshift(...selectedFacets); } //then add them to the convenient UI object (together with the exclusion property) uiData.push({ facets: facets, exclude: da.exclude === undefined ? false : da.exclude, field: da.field, title:// show user generated title or else the automated prettified title da.title || this.props.collectionConfig.toPrettyFieldName(da.field), empty: isEmptyAggr, //does the aggregation have anything in it index: index, // temporarily needed for guid guid: "facets__" + index }); }); return uiData; }; /*------------------------------------- FUNCTIONS FOR RENDERING ----------------------------*/ renderEmptyBlocks = aggr => ( <div className={IDUtil.cssClassName('empty-facet-block', this.CLASS_PREFIX)} key={"facet__" + aggr.index} id={"index__" + aggr.index}> <div className={IDUtil.cssClassName('title-wrapper', this.CLASS_PREFIX)}> <i className='fas fa-info-circle' data-for={'tooltip__' + aggr.index} data-tip={aggr.field} data-html={true} /> <div className={IDUtil.cssClassName('facet-title', this.CLASS_PREFIX)}> (0) {aggr.title}{" "} </div> <div className='fas fa-times' onClick={this.showRemoveDialog.bind( this, aggr.field, aggr.index )} /> </div> <ReactTooltip id={"tooltip__" + aggr.index}/> </div> ); renderSelectedFacet = (curAggr, f) => { let title = f.key + ' (' + curAggr.title + ')'; let count = f.count; if (curAggr.exclude === true) { title = "NOT - " + title; count = 0; } return ( <div key={'__sf__' + curAggr.field + f.key} className={classNames( { exclude: curAggr.exclude === true }, IDUtil.cssClassName('selected-item', this.CLASS_PREFIX) )}> <span className="elem-label" title={title}>{title}{" "}</span> <span className={IDUtil.cssClassName('count', this.CLASS_PREFIX)}> {count} </span> <span className="fas fa-times" onClick={this.toggleSelectedFacet.bind(this, curAggr.field, f.key)}/> </div> ); }; renderNewFacetModal = (allowHeavyFacets, fieldList, desiredFacets) => { const filteredFields = fieldList.filter( item => desiredFacets.findIndex(facet => facet.field === item.value) === -1 ); return ( <FlexModal size="large" elementId="field_select__modal" stateVariable="showModal" owner={this} title="Create a new facet" > <AggregationCreator allowHeavyFacets={allowHeavyFacets} onOutput={this.onComponentOutput} key={this.props.searchId} fieldList={filteredFields} /> </FlexModal> ); }; renderRemoveFacetModal = () => ( <FlexModal elementId="field_select_facet__modal" stateVariable="showModalWarning" owner={this} title="Remove current facet?" > <div> <p> You are removing the current facet " <u>{this.props.collectionConfig ? this.props.collectionConfig.toPrettyFieldName(this.currentFacet) : this.currentFacet }</u>". You can bring it back by using the "New" facet option and searching for the same field name again </p> <br/> <button type="button" onClick={this.removeAggregation} className="btn btn-primary"> Remove </button> </div> </FlexModal> ); render() { //contains all required data for generating the (empty) aggregation blocks and selected facets const uiData = this.generateUIData(); //modals const aggregationCreatorModal = this.state.showModal && this.props.collectionConfig ? this.renderNewFacetModal( this.props.allowHeavyFacets, this.props.collectionConfig.getFacetSelectionList(this.props.allowHeavyFacets), this.props.desiredFacets ) : null; const aggregationModalWarning = this.state.showModalWarning ? this.renderRemoveFacetModal() : null; //for each empty aggregation, add a (rendered) block to the list (of empty aggregations) const emptyAggrBlocks = uiData .filter(aggr => aggr.empty) .map(this.renderEmptyBlocks); //contains aggregations without any results const aggregationBlocks = []; let selectedFacets = []; //holds the list of selected facets to be displayed at the top //loop through the non-empty "desired aggregations" (non-histogram only) uiData .filter(aggr => !aggr.empty) .forEach(curAggr => { //add another (rendered) aggregation block to the list aggregationBlocks.push( <AggregationBox key={this.props.searchId + '__' + curAggr.guid} showRemoveDialog={this.showRemoveDialog} onOutput={this.onOutput} selectedFacets={this.props.selectedFacets} desiredFacets={this.props.desiredFacets} onToggleSelectedFacet={this.toggleSelectedFacet} data={curAggr} />); //add the (rendered) facets that are selected within the current aggregation //block (to be displayed at the top) selectedFacets = selectedFacets.concat( curAggr.facets.filter(f => f.selected).map( f => {return this.renderSelectedFacet(curAggr, f)} ) ); }); //finally render the whole thing return ( <div className={IDUtil.cssClassName("aggregation-list checkboxes")}> {aggregationCreatorModal} {aggregationModalWarning} {/* Create new facet */} <div className={IDUtil.cssClassName("tab-new", this.CLASS_PREFIX)}> <button className="btn" onClick={ComponentUtil.showModal.bind( this, this, "showModal" )} > <i className="fas fa-plus" /> Add a new facet </button> </div> {/* Selected/active facets */} <div className={IDUtil.cssClassName('selected-facets', this.CLASS_PREFIX)}> {selectedFacets} </div> {/* Empty facets */} {emptyAggrBlocks} {/* Facet list */} {aggregationBlocks} </div> ); } } AggregationList.propTypes = { aggregations: PropTypes.object.isRequired, collectionConfig: PropTypes.object.isRequired, desiredFacets: PropTypes.array, onOutput: PropTypes.func.isRequired, queryId: PropTypes.string, searchId: PropTypes.string, allowHeavyFacets: PropTypes.bool.isRequired, //for determining whether to show heavy facets or not selectedFacets: PropTypes.object };