UNPKG

labo-components

Version:
1,129 lines (979 loc) 37.3 kB
import React from 'react'; import PropTypes from 'prop-types'; import Project from './model/Project'; import Query from './model/Query'; import APIError from './model/APIError'; import SearchAPI from './api/SearchAPI'; import PlayoutAPI from './api/PlayoutAPI'; import ProjectAPI from './api/ProjectAPI'; import QueryAPI from './api/QueryAPI'; import AnnotationAPI from './api/AnnotationAPI'; import CollectionRegistryAPI from './api/CollectionRegistryAPI'; import IDUtil from './util/IDUtil'; import CollectionUtil from './util/CollectionUtil'; import ComponentUtil from './util/ComponentUtil'; import AnnotationUtil from './util/AnnotationUtil'; import LocalStorageHandler from './util/LocalStorageHandler'; import FlexModal from './components/FlexModal'; import FlexRouter from './util/FlexRouter'; import CollectionSelector from './components/collection/CollectionSelector'; import BookmarkSelector from './components/bookmark/BookmarkSelector'; import ToolHeader from './components/shared/ToolHeader'; import CollectionBar from './components/search/CollectionBar'; import QueryBuilder from './components/search/QueryBuilder'; import QueryEditor from './components/search/QueryEditor'; import SearchHit from './components/search/SearchHit'; import QuickViewer from './components/search/QuickViewer'; import Paging from './components/search/Paging'; import Sorting from './components/search/Sorting'; import MessageHelper from './components/helpers/MessageHelper'; import Loading from './components/shared/Loading'; import classNames from 'classnames'; import PaginationUtil from './util/PaginationUtil'; export const SINGLE_SEARCH_RESOURCE_VIEWER_POPUP = 'single-search-result-viewer'; export default class SingleSearchRecipe extends React.Component { constructor(props) { super(props); this.state = { initializing : true, showCollectionModal : false, //for the collection selector showBookmarkModal : false, //for the bookmark group selector showQuickViewModal : false, //for the quickview result preview savedBookmarkModal: false, collectionList : null, browseSelection : false, //always start off with the search results activeProject : null, userProjects : null, isPagingOutOfBounds: false, isQueryAccessDenied: false, isSearching : false, //awaiting the search API pageSize : 20, //amount of search results on page collectionConfig : null, //loaded after mounting, without it nothing works currentOutput: null, //contains the current search results initialQuery : null, //yikes this is only used for storing the initial query lastQuerySaved : null, selectedOnPage : {}, // key = resourceId, value = true/false; this is a subselection from the 'stored-selections', which contains selections from everywhere allRowsSelected : false, // are all search results selected gtaaId : null //selected entity from dropdown, passed through from SearchTermInput }; this.CLASS_PREFIX = "ssr"; PropTypes.checkPropTypes(SingleSearchRecipe.propTypes, this.props, 'prop', this.constructor.name); } static elementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) ) } static afterRenderingHits() { const imgDefer = document.getElementsByTagName('img'); for (let i=0; i<imgDefer.length; i++) { if(imgDefer[i].getAttribute('data-src') && SingleSearchRecipe.elementInViewport(imgDefer[i])) { imgDefer[i].setAttribute('src', imgDefer[i].getAttribute('data-src')); // prevent multiple conversions imgDefer[i].removeAttribute('data-src'); } } } componentWillUnmount() { window.onscroll = null; } componentDidMount = async () => { //makes sure that the images are loaded only when visible window.addEventListener('scroll', () => {SingleSearchRecipe.afterRenderingHits()}); this.init(); } //new init function that properly loads page content (and actually handles errors) init = async () => { //first construct a project from local storage (if any) const project = LocalStorageHandler.getJSONFromLocalStorage('stored-active-project') ? Project.construct(LocalStorageHandler.getJSONFromLocalStorage('stored-active-project')) : null ; //then see which query should be executed let initialQuery = null; let queryLoadError = -1; //everything is fine try { initialQuery = await this.determineInitialQuery( this.props.user, this.props.params ); } catch(err) { queryLoadError = err; } //if the initialQuery is null, use the ID of the last used collection //if this ends up being null that should be fine (only collection selector will show) const selectedCollectionId = initialQuery ? initialQuery.collectionId : LocalStorageHandler.getJSONFromLocalStorage('active-collection-id') ; //first get the collectionConfig const collectionConfig = selectedCollectionId ? await this.generateCollectionConfig( this.props.clientId, this.props.user, selectedCollectionId ) : null; //only construct an initial collection query if there is no specific load error if(!initialQuery && queryLoadError === -1) { initialQuery = Query.construct({size : this.state.pageSize}, collectionConfig); } //now load all the user's projects const userProjects = await this.loadUserProjects(this.props.user); //and the user's bookmarks //TODO do not load these bookmarks by default! It is very slow const bookmarks = await this.loadUserBookmarks(this.props.user, project); //and finally the list of collections const collectionList = await this.loadCollectionList(); this.setState({ initializing : false, //done initializing! activeProject : project, collectionConfig : collectionConfig, collectionId : selectedCollectionId, initialQuery : initialQuery, userProjects : userProjects, activeBookmarks : bookmarks, collectionList : collectionList, isQueryAccessDenied : queryLoadError !== -1, //NB either 403 or 404 at the moment! currentOutput : null }); }; // ---------------------------------- SYNCHRONOUS LOADING FUNCTIONS ------------------------- //TODO really make sure this works well. Also figure out if prioritised queries still are necessary! determineInitialQuery = async (user, urlParams) => { let initialQuery = null; if (urlParams && urlParams.queryId) { // the stored-priority-query else the query in stored-search-results if(urlParams.queryId === 'cache') { const storedResults = LocalStorageHandler.getJSONFromLocalStorage('stored-search-results'); initialQuery = storedResults ? storedResults.query : null; } else if (urlParams.queryId === 'prio') { //FIXME get rid of this weird type of query, see CollectionConfig.__getSearchReferences() //also see MetadataTable.performSearchReference() initialQuery = LocalStorageHandler.getJSONFromLocalStorage('stored-priority-query'); } else { // have a query ID const userQuery = await this.loadUserQuery(urlParams.queryId, user.id === 'ANONYMOUS' ? null : user.id , null); if(userQuery && userQuery.query && userQuery.queryType === 'layered search') { initialQuery = userQuery.query; } else if (userQuery instanceof APIError) { throw userQuery.toHTTPErrorCode(); } } } return initialQuery; }; generateCollectionConfig = (clientId, user, collectionId) => { return new Promise(resolve => { if (!clientId || !user || !collectionId) resolve(null); CollectionUtil.generateCollectionConfig( clientId, user, collectionId, resolve ); }); }; loadUserProject = (user, projectId) => { return new Promise(resolve => { if (!user || !user.id || !projectId) resolve(null); ProjectAPI.get(user.id, projectId, resolve); }); }; loadUserQuery = (queryId, user, projectId) => { return new Promise(resolve => { if (!queryId) resolve(null); QueryAPI.get(queryId, user, projectId, resolve); }); }; loadUserProjects = user => { return new Promise(resolve => { if (!user || !user.id) resolve([]); //always make sure to return a list ProjectAPI.list(user.id, null, resolve); }); }; loadUserBookmarks = (user, project) => { return new Promise(resolve => { if(!project || !project.id || !user || !user.id) resolve(null); AnnotationAPI.getBookmarks(user.id, project.id, resolve); }); }; loadCollectionList = () => { return new Promise(resolve => { CollectionRegistryAPI.listCollections(resolve); }) }; onLoadPlayoutAccess = (accessApproved, desiredState) => { this.setState( desiredState, () => { if(desiredState.currentOutput === null) { //clear the stored query and start a fresh single search query LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-search-results'); FlexRouter.gotoSingleSearch() } // show media visible on screen SingleSearchRecipe.afterRenderingHits(); } ); }; /* ------------------------------- CHILD COMPONENT CALLBACKS ----------------------- */ //NOTE: the original idea was to control the output of all child components in this function and orchestrate what to do based on the recipe onComponentOutput = (componentClass, data) => { if(componentClass === 'QueryBuilder') { this.onSearched(data); } else if (componentClass === 'CollectionSelector') { this.onCollectionSelected(data); } else if(componentClass === 'SearchHit') { this.onItemSelected(data.resource, data.selected); } else if(componentClass === 'BookmarkSelector') { this.onBookmarkGroupSelected(data); } else if(componentClass === 'QueryEditor') { this.onQuerySaved(data) } else if(componentClass === 'CollectionBar') { this.onSearched(data); //data is always null (used to reset the search...) } else if(componentClass === 'ToolHeader') { this.onProjectSelected(data) } }; onCollectionSelected = collectionMetadata => { const collectionConfig = CollectionUtil.createCollectionConfig( this.props.clientId, this.props.user, collectionMetadata.index, collectionMetadata ); //set the default query for the selected collection; creates a new query builder LocalStorageHandler.storeJSONInLocalStorage('active-collection-id', collectionConfig.collectionId); const previousSearchTerm = this.state.currentOutput && this.state.currentOutput.query ? this.state.currentOutput.query.term : null; this.setState( { collectionId: collectionConfig.collectionId, collectionConfig: collectionConfig, initialQuery: Query.construct({size: this.state.pageSize, term : previousSearchTerm}, collectionConfig), currentOutput: null, browseSelection: false, isQueryAccessDenied: false }, () => { ComponentUtil.hideModal(this, 'showCollectionModal', 'collection__modal', true); } ); }; onProjectSelected = async (project) => { this.reloadBookmarks(this.props.user, project); LocalStorageHandler.storeJSONInLocalStorage('stored-active-project', project); }; onItemSelected = (searchResult, isSelected) => { if(searchResult) { //update the list of selected items (showing on the page) const selectedOnPage = this.state.selectedOnPage; if(isSelected) { selectedOnPage[searchResult.resourceId] = true; } else { delete selectedOnPage[searchResult.resourceId] } // make sure to update the list of stored bookmarks with the changed selection this.updateSelectedItems(searchResult, isSelected); //determine whether the last selected item in the selection was selected, so we need to switch back to showing the result list let browseSelection = this.state.browseSelection; if(browseSelection) { const selRowsInLocalStorage = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') || []; browseSelection = selRowsInLocalStorage.length > 0; } this.setState({ selectedOnPage : selectedOnPage, allRowsSelected : isSelected ? this.areAllItemsSelected() : false, browseSelection : browseSelection, }); } }; /* ------------------------------- SEARCH RELATED FUNCTIONS ----------------------- */ onStartSearch = () => { this.setState({ isSearching : true }) }; onSearched = (resultsObj, activeResource=null, paging=false) => { const desiredState = { currentOutput: resultsObj, browseSelection : false }; // if search is not the result of paging then clear selectedOnPage. !paging ? desiredState.selectedOnPage = {} : desiredState; //reset the poster images to the placeholder const imgDefer = document.getElementsByTagName('img'); for (let i=0; i<imgDefer.length; i++) { if(imgDefer[i].getAttribute('data-src')) { imgDefer[i].setAttribute('src', '/static/images/placeholder.2b77091b.svg'); } } //request access for the thumbnails if needed if (this.state.collectionConfig.requiresPlayoutAccess() && this.state.collectionConfig.getThumbnailContentServerId()) { PlayoutAPI.requestAccess( this.state.collectionConfig.getThumbnailContentServerId(), 'thumbnails', desiredState, this.onLoadPlayoutAccess ) } else { this.onLoadPlayoutAccess(true, desiredState); } this.setState({ selectedOnPage : this.getAlreadySelectedItems(resultsObj), allRowsSelected: this.areAllItemsSelected(), isSearching: false, isPagingOutOfBounds : resultsObj ? resultsObj.pagingOutOfBounds === true : false }) //If the quickview modal was open during the loading of the new page of results... //It has to pop up again after loading new results. //TODO merge with the desiredState if(this.state.showQuickViewModal && activeResource) { this.openQuickViewModal(activeResource); } }; gotoPage = pageNumber => { this.setState({ isSearching : true }, () => { if(this.state.currentOutput && this.state.currentOutput.query) { PaginationUtil.loadSearchResultPage( this.state.currentOutput, this.state.collectionConfig, pageNumber, this.onPaged ); } }) }; onPaged = (activeResource, newSearchResults) => { if(this.state.showQuickViewModal) { this.openQuickViewModal(activeResource); //makes sure the quick viewer stays open } if(newSearchResults) { //make sure the owner get's notified of new search results this.onSearched(newSearchResults, activeResource, true) //calls the owners onSearched } } //NOTE: The sortMode is translated to sort params inside the QueryBuilder component //sortMode = {type : date/rel, order : desc/asc} sortResults = (queryId, sortParams) => { this.setState({ isSearching : true }, () => { if (this.state.currentOutput) { const sr = this.state.currentOutput; sr.query.sort = sortParams; sr.query.offset = 0; SearchAPI.search(sr.query, sr.collectionConfig, data => this.onSearched(data), true) } }) }; /* ------------------------------- TABLE ACTION FUNCTIONS ----------------------- */ //hides search results and shows all selected items browseSelection = () => { this.setState({ browseSelection : !this.state.browseSelection }, ()=>{ // show media visible on screen SingleSearchRecipe.afterRenderingHits(); }); }; //checks if the search results contain resources that were already selected in another query getAlreadySelectedItems = resultsObj => { //instance of SearchResults if(!resultsObj || !resultsObj.results) { return {}; } const rowsInStorage = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') || []; const selRows = {}; resultsObj.results.forEach(searchResult => rowsInStorage.filter(obj => { if(obj.resourceId === searchResult.resourceId) { selRows[searchResult.resourceId] = true; }} )); return selRows; }; //TODO check this function, it is a bit duplicate, similar code is also in ... areAllItemsSelected = () => { const storeSelectedRows = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') if(!storeSelectedRows || !(this.state.currentOutput && this.state.currentOutput.results)) { return false; } return this.state.currentOutput.results.every( itemOnPage => storeSelectedRows.find(storedObj => storedObj.resourceId === itemOnPage.resourceId) ); }; clearSelectedItems = () => { LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-selections'); this.setState({ selectedOnPage : {}, allRowsSelected : false, browseSelection : false, }); }; toggleSelectAllItems = e => { e.preventDefault(); let rows = this.state.selectedOnPage; const rowsOnLocalStorage = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') || null; if(this.state.allRowsSelected) { this.state.currentOutput.results.forEach(result => { const isChecked = Object.keys(rows).findIndex(resourceId => resourceId === result.resourceId); if(isChecked !== -1) { LocalStorageHandler.removeItemInLocalStorage('stored-selections', result, 'resourceId'); } }); rows = {}; } else { this.state.currentOutput.results.forEach(result => { rows[result.resourceId] = !this.state.allRowsSelected; const isChecked = rowsOnLocalStorage ? rowsOnLocalStorage.findIndex( storedObj => storedObj.resourceId === result.resourceId ) : -1; if(isChecked === -1) { this.updateSelectedItems(result, true); } }); } this.setState({ allRowsSelected : !this.state.allRowsSelected, selectedOnPage : rows }); }; updateSelectedItems = (resource, select) => { if(select) { resource.query = this.state.currentOutput.query; LocalStorageHandler.pushItemToLocalStorage('stored-selections', resource, 'resourceId'); } else { LocalStorageHandler.removeItemInLocalStorage('stored-selections', resource, 'resourceId'); } }; /* ------------------------------- BOOKMARK RELATED FUNCTIONS ----------------------- */ reloadBookmarks = async (user, project) =>{ const bookmarks = await this.loadUserBookmarks(user, project); this.setState({ activeBookmarks : bookmarks, activeProject : project}) }; bookmarkSelectedItems = () => this.setState({showBookmarkModal : true, showBookmarkItems : false, browseSelection : false}); // makes sure that all selected resources are ADDED to the selected groups bookmarkToGroupInProject = (allGroups, selectedGroups, selectedProject) => { const selectedRows = LocalStorageHandler.getJSONFromLocalStorage('stored-selections'); ComponentUtil.hideModal(this, 'showBookmarkModal', 'bookmark__modal', true, () => { let saveCount = 0; //run through all the selected groups allGroups.filter(group => selectedGroups[group.id] === true).forEach(group => { //then add all the selected resources to the group's list of targets const targets = group.target.concat( selectedRows.map(result => AnnotationUtil.generateResourceLevelTarget( result.collectionId, result.resourceId )) ); //make sure to remove duplicate targets (could happen in case a target was already in a group) const temp = {}; const dedupedTargets = []; targets.forEach((t) => { if(!temp[t.source]) { temp[t.source] = true; dedupedTargets.push(t); } }); group.target = dedupedTargets; //FIXME this code is not entirely safe: what if somehow the saveAnnotation does not return? AnnotationAPI.saveAnnotation(group, () => { if(++saveCount === Object.keys(selectedGroups).length) { this.onSaveBookmarks(selectedProject); } }); }); }); }; onBookmarkGroupSelected = data => { if(data && data.allGroups && data.selectedGroups && data.selectedProject) { this.bookmarkToGroupInProject(data.allGroups, data.selectedGroups, data.selectedProject); } }; onSaveBookmarks = (selectedProject) => { this.setState({ selectedOnPage : {}, allRowsSelected : false, browseSelection : false, savedBookmarkModal: true, activeProject: selectedProject }, () => { this.reloadBookmarks(this.props.user, selectedProject); LocalStorageHandler.removeJSONByKeyInLocalStorage('stored-selections'); }) }; /* ------------------------------- QUERY SAVING RELATED FUNCTIONS ----------------------- */ saveQuery = () => this.setState({showQueryModal : true}); onQuerySaved = data => { if(!data) return; ComponentUtil.hideModal(this, 'showQueryModal', 'query__modal', true, () => { this.setState({ savedQueryModal : true, lastQuerySaved : data.queryName, activeProject: data.project }); }); }; /* ------------------------------- QUICKVIEW FUNCTIONS -------------------------- */ openQuickViewModal = searchResult => { this.setState({ quickViewData: searchResult, //instance of SearchResult showQuickViewModal : true }); }; /* -------------------------------- RENDER THE MODALS --------------------------*/ renderCollectionModal = () => { return ( <FlexModal elementId="collection__modal" stateVariable="showCollectionModal" owner={this} size="large" title="Choose a collection"> <CollectionSelector onOutput={this.onComponentOutput} showSelect={true} collectionList={this.state.collectionList} showBrowser={true}/> </FlexModal> ); }; renderQuickViewModal = (browseSelection, selected, searchResult, collectionConfig) => { //quickViewData is an instance of SearchResult return ( <FlexModal elementId="quickview__modal" stateVariable="showQuickViewModal" owner={this} size="large" title={searchResult.title}> <QuickViewer browseSelection={browseSelection} // should the quickviewer page through the selection or result list searchResult={searchResult} // i.e SearchResult selected={selected} // if the current quick view result is in the stored rows collectionConfig={collectionConfig} onPaged={this.onPaged} //called after a page action has been completed onLoading={this.onStartSearch} // needs to be called back on paging to a new search result page onSelect={this.onItemSelected} // same function as used by the search hit /> </FlexModal> ) }; onGotoResourceViewer = (data, query, unique) => { if(data.resourceId) { //FlexRouter.popupResourceViewer(this.props.recipe.ingredients.resourceViewerPath, data, query.term,SINGLE_SEARCH_RESOURCE_VIEWER_POPUP + (unique ? data.resourceId : ''),false, () => this.reloadBookmarks(this.props.user, this.state.activeProject)); FlexRouter.gotoResourceViewer('/tool/resource-viewer', data, query.term); } }; renderQueryModal = (currentOutput, activeProject, userProjects) => { const projectTitle = activeProject ? `Save query parameters to your user project: ${activeProject.name}` : 'Save query parameters to a new user project'; currentOutput.query.id = null; //as saving, is a new query so reset id return ( <FlexModal elementId="query__modal" stateVariable="showQueryModal" owner={this} size="large" title={projectTitle}> <QueryEditor query={currentOutput.query} userProjects={userProjects} collectionConfig={currentOutput.collectionConfig} user={this.props.user} project={activeProject} onOutput={this.onComponentOutput}/> </FlexModal> ); }; renderBookmarkModal = (collectionConfig, activeProject, userProjects) => { return ( <FlexModal elementId="bookmark__modal" stateVariable="showBookmarkModal" owner={this} size="large" title="Bookmark your selection"> <BookmarkSelector onOutput={this.onComponentOutput} user={this.props.user} project={activeProject} userProjects={userProjects} collectionId={collectionConfig.collectionId} /> </FlexModal> ) }; renderSavedBookmarkModal = () => ( <FlexModal elementId="saved-bookmark__modal" stateVariable="savedBookmarkModal" owner={this} size="large" title="Bookmarks saved successfully"> Your selection of bookmarks were saved succesfully to project &quot;{this.state.activeProject.name}&quot; <p><a target="_blank" rel="noopener noreferrer" className="bg__workspace-bookmarks-link" href={"/workspace/projects/" + this.state.activeProject.id + "/bookmarks"}>Go to saved bookmarks</a></p> </FlexModal> ); renderSavedQueryModal = () => ( <FlexModal elementId="query-saved__modal" stateVariable="savedQueryModal" owner={this} size="large" title="Query saved successfully"> <p>Your query ({this.state.lastQuerySaved}) was saved successfully to project &quot;{this.state.activeProject.name}&quot;</p> <p><a target="_blank" rel="noopener noreferrer" className="bg__workspace-queries-link" href={"/workspace/projects/" + this.state.activeProject.id + "/queries"}>Go to saved queries</a></p> </FlexModal> ); /* --------------------------- RENDER HEADER --------------------- */ renderHeader = (name, activeProject, userProjects, user) => ( <ToolHeader name={name} activeProject={activeProject} projects={userProjects} user={user} onOutput={this.onComponentOutput} /> ); /* --------------------------- RENDER COLLECTION BAR --------------------- */ renderCollectionBar = (user, collectionConfig, activeProject) => ( <CollectionBar user={user} collectionConfig={collectionConfig} selectCollection={ComponentUtil.showModal.bind(this, this, 'showCollectionModal')} resetSearch={this.onComponentOutput} saveQuery={this.saveQuery} /> ); /* --------------------------- RENDER RESULT LIST ----------------------------*/ renderResultTable = (state, storedSelectedRows, visitedResults) => { //only render when there is a collectionConfig and searchAPI output if(!( state.collectionId && state.collectionConfig && state.currentOutput && state.currentOutput.results && state.currentOutput.results.length > 0 )) { return null } let listComponent = null; if (state.browseSelection) {//storedSelectedRows && storedSelectedRows.length > 0 listComponent = this.renderSelectionOverview(storedSelectedRows, state.currentOutput.query, state.collectionConfig, visitedResults) } else { //populate the list of search results listComponent = state.currentOutput.results.map( (result, index) => this.renderSearchResult( result, index, state.currentOutput.query, state.collectionConfig, storedSelectedRows, state.activeBookmarks, visitedResults.includes(result.resourceId) ) ); } const tableHeader = this.renderTableHeader( state.currentOutput, state.collectionConfig, storedSelectedRows, state.browseSelection, state.selectedOnPage, state.pageSize ); const tableFooter = this.renderTableFooter( state.currentOutput, state.pageSize ); return ( <div className={classNames(IDUtil.cssClassName('result-list', this.CLASS_PREFIX))}> {tableHeader} {listComponent} {tableFooter} </div> ) }; /* --------------------------- RENDERING LISTS -------------------------------*/ renderSelectionOverview = (storedSelectedRows, query, collectionConfig, visitedResults) => { return ( <div className="table-actions-header bookmarked-results"> <h4 className="header-selected-items"> Selected items <i className="fas fa-times" onClick={this.browseSelection}/> </h4> <div className="selected-items"> {storedSelectedRows.map((result, index) => { return this.renderSelectedItems(result, index, query, collectionConfig, visitedResults.includes(result.resourceId)) })} </div> </div> ); }; renderSelectedItems = (result, index, query, collectionConfig, visited) => { return ( <SearchHit key={'saved__' + index} searchResult={result} //instance of SearchResult collectionConfig={collectionConfig} query={query} visited={visited} bookmarked={null} selectable={this.state.activeProject ? true : false} isSelected={true} onQuickView={this.openQuickViewModal} onOutput={this.onComponentOutput} onGotoResourceViewer={this.onGotoResourceViewer} /> ) }; renderSearchResult = (result, index, query, collectionConfig, storedSelectedRows, activeBookmarks, visited) => { const bookmark = activeBookmarks ? activeBookmarks.find(item => item.resourceId === result.resourceId) : null; const isSelectedItem = storedSelectedRows.find(item => item.resourceId === result.resourceId) !== undefined; return ( <SearchHit key={'__' + index} searchResult={result} collectionConfig={collectionConfig} query={query} visited={visited} bookmark={bookmark} selectable={this.state.activeProject ? true : false} isSelected={isSelectedItem} onQuickView={this.openQuickViewModal} onOutput={this.onComponentOutput} onGotoResourceViewer={this.onGotoResourceViewer} /> ) }; /* ---------------------------- RENDER THE TABLE ------------------------- */ renderPagingButtons = (currentPage, totalHits, pageSize) => { if(currentPage <= 0) { return null; } return ( <Paging currentPage={currentPage} numPages={Math.ceil(totalHits / pageSize)} gotoPage={this.gotoPage} /> ) }; renderSortButtons = (collectionConfig, query) => { if(!query.sort) { return null; } return ( <Sorting sortResults={this.sortResults} sortParams={query.sort} collectionConfig={collectionConfig} dateField={query.dateRange ? query.dateRange.field : null} /> ) }; //FIXME the ugly HTML & lack of proper class names renderTableHeader = (currentOutput, collectionConfig, storedSelectedRows, browseSelection, selectedOnPage, pageSize) => { return ( <div className="table-actions-header"> {this.renderTableSelectAll(currentOutput, selectedOnPage)} {this.renderTableDropdown(storedSelectedRows, browseSelection)} <div style={{textAlign: 'center'}}> {this.renderPagingButtons(currentOutput.currentPage, currentOutput.totalHits, pageSize)} <div style={{float: 'right'}}> {this.renderSortButtons(collectionConfig, currentOutput.query)} </div> </div> </div> ) }; renderTableFooter = (currentOutput, pageSize) => { return ( <div className="table-actions-footer"> {this.renderPagingButtons(currentOutput.currentPage, currentOutput.totalHits, pageSize)} </div> ) }; renderTableSelectAll = (currentOutput, selectedOnPage) => { const currentSelectedIds = this.state.selectedOnPage ? Object.keys(this.state.selectedOnPage) : [] const allChecked = currentOutput.results.map(item => currentSelectedIds.findIndex(it => it === item.resourceId)); const isChecked = allChecked.findIndex(item => item === -1) === -1; return ( <div title={"Select " + (isChecked ? "none" : "all")} onClick={this.toggleSelectAllItems} className="select-all"> <input type="checkbox" readOnly={true} checked={isChecked ? 'checked' : ''} id={'cb__select-all'} /> <label htmlFor={'cb__select-all'}><span/></label> </div> ); }; renderTableDropdown = (storedSelectedRows, browseSelection) => { let dropdown = null; if(storedSelectedRows && storedSelectedRows.length > 0) { const actions = ( <div className="dropdown-menu" aria-labelledby="dropdownBookmarking"> <button className="dropdown-item" type="button" onClick={this.browseSelection}> {browseSelection ? 'Hide' : 'Show'} selected item(s) </button> <button className="dropdown-item" type="button" onClick={this.bookmarkSelectedItems}> Bookmark selection </button> <button className="dropdown-item" type="button" onClick={this.clearSelectedItems}> Clear selection </button> </div> ); dropdown = ( <div className="dropdown bookmark-dropdown-menu"> <button className="btn btn-secondary dropdown-toggle" type="button" id="dropdownBookmarking" data-toggle="dropdown" title="Selected items to bookmark" aria-haspopup="true" aria-expanded="false"> <i className="fas fa-check-square" style={{color: 'white', fontSize:'16px', paddingRight:'12px'}} />{storedSelectedRows.length} </button> {actions} </div> ); } return ( <div className="table-actions"> {dropdown} </div> ); }; /* ---------------------------- RENDER QUERY BUILDER -------------------------- */ renderSearchComponent = ( collectionConfig, initialQuery, isSearching, resultList, isPagingOutOfBounds, isQueryAccessDenied, pageSize ) => { const loadingMessage = isSearching ? <Loading message="Loading results..."/> : null; const queryBuilder = ( <QueryBuilder key={collectionConfig.getCollectionId()} //for resetting all the states held within after selecting a new collection header={true} aggregationView={this.props.recipe.ingredients.aggregationView} dateRangeSelector={true} showTimeLine={true} showKeywordHistogram={true} query={initialQuery} collectionConfig={collectionConfig} onStartSearch={this.onStartSearch} onOutput={this.onComponentOutput} resultList={resultList} //inject the resultlist, so it can be properly positioned isPagingOutOfBounds={isPagingOutOfBounds} isQueryAccessDenied={isQueryAccessDenied} pageSize={pageSize} /> ); return ( <div className="search-component"> {loadingMessage} {queryBuilder} </div> ) }; /* ---------------------------- MAIN RENDER FUNCTION -------------------------- */ showHelp = () => { const event = new Event('BG__SHOW_HELP'); window.dispatchEvent(event); }; /* ---------------------------- MAIN RENDER FUNCTION -------------------------- */ render() { if(this.state.initializing) return <Loading message="Initializing collection & user data..."/>; const storedSelectedRows = LocalStorageHandler.getJSONFromLocalStorage('stored-selections') || []; const visitedResults = LocalStorageHandler.getJSONFromLocalStorage('stored-visited-results') || []; // all the different modals const collectionModal = this.state.showCollectionModal && this.state.collectionList ? this.renderCollectionModal() : null; const quickViewModal = this.state.showQuickViewModal && this.state.quickViewData ? this.renderQuickViewModal( this.state.browseSelection, storedSelectedRows.findIndex(elem => elem.resourceId === this.state.quickViewData.resourceId) >= 0, this.state.quickViewData, this.state.collectionConfig ) : null; const queryModal = this.state.showQueryModal && this.state.currentOutput && this.state.userProjects ? this.renderQueryModal( this.state.currentOutput, this.state.activeProject, this.state.userProjects ) : null; const bookmarkModal = this.state.showBookmarkModal && this.state.userProjects && this.state.userProjects.length > 0 ? this.renderBookmarkModal( this.state.collectionConfig, this.state.activeProject, this.state.userProjects ) : null; const header = this.renderHeader( this.props.recipe.name, this.state.activeProject, this.state.userProjects, this.props.user ); // Component loaded only after header <ToolHeader> is rendered // to avoid 'glitch' when 'Collection Selection btn' loads first // because of async loading. const collectionBar = header ? this.renderCollectionBar( this.props.user, this.state.collectionConfig, this.state.activeProject ) : null; const savedQueryModal = this.state.savedQueryModal ? this.renderSavedQueryModal() : null; const savedBookmarkModal = this.state.savedBookmarkModal ? this.renderSavedBookmarkModal() : null; //the query builder & loading message const searchComponent = this.state.collectionConfig ? this.renderSearchComponent( this.state.collectionConfig, this.state.initialQuery, this.state.isSearching, this.renderResultTable(this.state, storedSelectedRows, visitedResults), this.state.isPagingOutOfBounds, this.state.isQueryAccessDenied, this.state.pageSize ) : null; return ( <div className={IDUtil.cssClassName('single-search-recipe')}> {header} {collectionBar} {searchComponent} {collectionModal} {queryModal} {bookmarkModal} {quickViewModal} {savedQueryModal} {savedBookmarkModal} </div> ); } } SingleSearchRecipe.propTypes = { clientId : PropTypes.string, user: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string, attributes: PropTypes.shape({ allowPersonalCollections: PropTypes.bool }) }), params: PropTypes.shape({ queryId: PropTypes.string }).isRequired, recipe: PropTypes.shape({ description: PropTypes.string, id: PropTypes.string, inRecipeList: PropTypes.bool, name: PropTypes.string.isRequired, // Use for when rendering header (isRequired by <ToolHeader/>) phase: PropTypes.string, recipeDescription: PropTypes.string, type: PropTypes.string, url: PropTypes.string, ingredients: PropTypes.shape({ resourceViewerPath: PropTypes.string.isRequired, collection: PropTypes.string, collectionSelector: PropTypes.bool, dateRangeSelector: PropTypes.string.isRequired, // Use by <QueryBuilder/> to render Date Controls aggregationView: PropTypes.string, useProjects: PropTypes.bool }).isRequired }).isRequired }; SingleSearchRecipe.defaultProps = { queryId: 'cache' };