labo-components
Version:
1,129 lines (979 loc) • 37.3 kB
JSX
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 "{this.state.activeProject.name}"
<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 "{this.state.activeProject.name}"</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'
};