UNPKG

labo-components

Version:
486 lines (429 loc) 17.6 kB
import AnnotationAPI from '../../../../api/AnnotationAPI'; import IDUtil from '../../../../util/IDUtil'; import BulkActions from '../../helpers/BulkActions'; import { createOptionList, createAnnotationClassificationOptionList } from '../../helpers/OptionList'; import { exportDataAsJSON } from '../../helpers/Export'; import ResourceViewerModal from '../../ResourceViewerModal'; import NestedTable from '../../helpers/NestedTable'; import AnnotationRow from './AnnotationRow'; import classNames from 'classnames'; import PropTypes from 'prop-types'; /** * This view handles the loading, filtering and selection of data of * the Annotations list of a project. It is displayed using the NestedTable component. */ class AnnotationTable extends React.PureComponent { constructor(props) { super(props); this.title = props.title; this.orders = this.getOrders(); this.bulkActions = [ { title: 'Delete', onApply: this.deleteAnnotations.bind(this) }, { title: 'Export', onApply: this.exportAnnotations.bind(this) } ]; this.state = { parentAnnotations : null, annotations: [], //these are actually annotation bodies (so objects from annotation['body']) selection: [], loading: true, detailBookmark: null, filters: [], showSub: {} }; // bind functions (TODO get rid of these, they are unnecessary and confusing) this.closeItemDetails = this.closeItemDetails.bind(this); this.deleteAnnotations = this.deleteAnnotations.bind(this); this.deleteAnnotation = this.deleteAnnotation.bind(this); this.exportAnnotations = this.exportAnnotations.bind(this); this.exportAnnotation = this.exportAnnotation.bind(this); this.filterAnnotations = this.filterAnnotations.bind(this); this.renderResults = this.renderResults.bind(this); this.selectAllChange = this.selectAllChange.bind(this); this.selectItem = this.selectItem.bind(this); this.sortAnnotations = this.sortAnnotations.bind(this); this.viewBookmark = this.viewBookmark.bind(this); this.toggleSub = this.toggleSub.bind(this); this.unFoldAll = this.unFoldAll.bind(this); this.foldAll = this.foldAll.bind(this); } componentDidMount() { this.loadAnnotations(); } loadAnnotations() { AnnotationAPI.getAnnotationBodies( this.props.user.id, this.props.project.id, this.props.type, (annotations) => { if(annotations) { this.setState({ parentAnnotations: null, //do we still need this for deleting an annotation? annotations: annotations, loading: false, filters: this.getFilters(annotations) }, () => { this.updateSelection(annotations) }) } } ) } //Get sort orders getOrders(items){ const sortNames = { 'created': 'Created at', // a-z 'a-z-label': 'A-Z', 'a-z-text': 'A-Z', // z-a 'z-a-label': 'Z-A', 'z-a-text': 'Z-A', 'vocabulary': 'Vocabulary', 'template': 'Template', }; return this.props.sort.map((sort)=>({ value: sort, name: (sort in sortNames) ? sortNames[sort] : '!! ' + sort }) ); } sortAnnotations(annotations, field) { const safeToLowerCase= (s)=>( s && typeof s === 'string' ? s.toLowerCase() : '' ); switch (field) { case 'created': return annotations.sort((a, b) => a.created < b.created ? 1 : -1); case 'a-z-label': return annotations.sort((a, b) => safeToLowerCase(a.label) > safeToLowerCase(b.label) ? 1 : -1); case 'z-a-label': return annotations.sort((a, b) => safeToLowerCase(a.label) < safeToLowerCase(b.label) ? 1 : -1); case 'a-z-text': return annotations.sort((a, b) => safeToLowerCase(a.text) > safeToLowerCase(b.text) ? 1 : -1); case 'z-a-text': return annotations.sort((a, b) => safeToLowerCase(a.text) < safeToLowerCase(b.text) ? 1 : -1); case 'vocabulary': return annotations.sort((a, b) => safeToLowerCase(a.vocabulary) > safeToLowerCase(b.vocabulary) ? 1 : -1); case 'template': return annotations.sort((a, b) => safeToLowerCase(a.template)> safeToLowerCase(b.template) ? 1 : -1); default: return annotations; } } //Get filter object getFilters(items) { return this.props.filters.map((filter)=>{ switch(filter){ case 'search': // search filter return { title:'', key: 'keywords', type: 'search', placeholder: 'Search Annotations' }; case 'vocabulary': return { title:'Vocabulary', key: 'vocabulary', type: 'select', options: createOptionList(items, (i)=>(i['vocabulary']) ) }; case 'bookmarkGroup': return { title:'Group', titleAttr: 'Bookmark group', key: 'bookmarkGroup', type: 'select', options: createAnnotationClassificationOptionList(items, 'groups'), }; case 'classification': return { title:'Code', titleAttr: 'Bookmark code', key: 'bookmarkClassification', type: 'select', options: createAnnotationClassificationOptionList(items, 'classifications'), }; default: console.error('Unknown filter preset', filter); } }) } //Update Selection list, based on available items updateSelection(items) { this.setState({ selection: items.filter(item => this.state.selection.some((i)=>(i.annotationId === item.annotationId))) }); } filterByKeyValue(items, getValue, value) { return items.filter((i)=>(getValue(i) === value)) }; //Filter annotation list by given filter filterAnnotations(annotations, filter) { // check the annotation vocabulary if (filter.vocabulary){ annotations = annotations.filter((a)=>(a.vocabulary === filter.vocabulary)); } // check the groups for each bookmark of each annotation if (filter.bookmarkGroup){ annotations = annotations.filter((a)=>( a.bookmarks.some( (b)=>( b.groups.some( (c) => (c.annotationId === filter.bookmarkGroup) ) ) ) ) ); } // check the classifications for each bookmark of each annotation if (filter.bookmarkClassification){ annotations = annotations.filter((a)=>( a.bookmarks.some( (b)=>( b.classifications.some( (c) => (c.annotationId === filter.bookmarkClassification) ) ) ) ) ); } // filter on keywords in title, dataset or type if (filter.keywords) { const keywords = filter.keywords.split(' '); keywords.forEach(k => { k = k.toLowerCase(); annotations = annotations.filter( annotation => // annotation (Object.keys(annotation).some((key)=>( typeof annotation[key] === 'string' && annotation[key].toLowerCase().includes(k)) )) || // bookmarks (annotation.bookmarks && annotation.bookmarks.some((bookmark)=>( // bookmark Object.keys(bookmark).some((key)=>(typeof bookmark[key] === 'string' && bookmark[key].toLowerCase().includes(k))) // bookmark-object || Object.keys(bookmark.object).some((key)=>(typeof bookmark.object[key] === 'string' && bookmark.object[key].toLowerCase().includes(k))) // bookmark-groups || bookmark.groups.some((g)=>(g.label.toLowerCase().includes(k))) // bookmark-groups || bookmark.classifications.some((g)=>(g.label.toLowerCase().includes(k))) )) ) ); }); } return annotations; } deleteAnnotations(annotationBodies) { if(annotationBodies) { // always ask before deleting let msg = 'Are you sure you want to remove the selected annotation'; msg += annotationBodies.length === 1 ? '?' : 's?'; if (!confirm(msg)) { return; } //populate the deletion list required for the annotation API const deletionList = []; annotationBodies.forEach(body => { body.bodyObjects.forEach(annotationBody => { deletionList.push({ annotationId : annotationBody.parentAnnotationId, type : 'body', partId : annotationBody.annotationId }) }) }); //now delete all the annotations with a single call to the annotation API AnnotationAPI.deleteUserAnnotations( this.props.user.id, deletionList, (success) => { setTimeout(()=>{ // load new data this.loadAnnotations(); // update bookmark count in project menu this.props.loadBookmarkCount(); }, 500); } ); } } deleteAnnotation(annotation){ //this.deleteAnnotations([annotation.annotationId]); this.deleteAnnotations([annotation]); } exportAnnotations(annotations) { let data = this.state.annotations.filter(item => annotations.includes(item) ); // remove cyclic structures data = data.map(d => { d.bookmarks.forEach((b)=>{ delete b.groups; delete b.classifications; }); return d; }); exportDataAsJSON(data); } exportAnnotation(annotation){ this.exportAnnotations([annotation]); } viewBookmark(bookmark) { this.setState({ detailBookmark: bookmark }); } //Close itemDetails view, and refresh the data (assuming changes have been made) closeItemDetails() { // set viewbookmark to null this.viewBookmark(null); // refresh data this.loadAnnotations(); // update bookmark count in project menu this.props.loadBookmarkCount(); } sortChange(e) { this.setSort(e.target.value); } //TODO test this function selectAllChange(selectedItems, e) { const newSelection = this.state.selection.slice().filter((i)=>(selectedItems.includes(i))); //copy the array selectedItems.forEach(item => { const found = newSelection.find(selected => selected.annotationId === item.annotationId) if(!found && e.target.checked) { // add it to the selection newSelection.push(item); } else if (!e.target.checked && found) { // remove the selected item newSelection.splice(found, 1); } }); this.setState({ selection: newSelection }); } //TODO test this function selectItem(item, select) { const newSelection = this.state.selection.slice(); //copy the array const index = newSelection.findIndex(selected => { return selected.annotationId === item.annotationId }); if(index === -1 && select) { // add it to the selection newSelection.push(item); } else if (!select && index !== -1) { // remove the selected item newSelection.splice(index, 1); } this.setState({ selection: newSelection }); } // Toggle sublevel visibility toggleSub(id){ const showSub = Object.assign({}, this.state.showSub); if (id in showSub){ delete showSub[id]; } else{ showSub[id] = true; } this.setState({showSub}); } unFoldAll(){ const showSub = {}; this.state.annotations.forEach((b)=>{ if (b.bookmarks && b.bookmarks.length > 0){ showSub[b.annotationId] = true; } }); this.setState({showSub}); } foldAll(){ this.setState({showSub:{}}); } renderResults(renderState) { return ( <div> <h2> <input type="checkbox" checked={ renderState.visibleItems.length > 0 && renderState.visibleItems.every(item => this.state.selection.some((i)=>(i.annotationId == item.annotationId)) ) } onChange={this.selectAllChange.bind(this, renderState.visibleItems)}/> {this.title} {this.state.renders} :{' '}<span className="count">{renderState.visibleItems.length || 0}</span> <div className="fold"> <div className="filter"> <span onClick={this.unFoldAll}>Show all bookmarks</span> / <span onClick={this.foldAll}>Hide all bookmarks</span> </div> </div> </h2> <div className="bookmark-table"> {renderState.visibleItems.length ? renderState.visibleItems.map((annotation, index) => ( <AnnotationRow key={annotation.annotationId} annotation={annotation} onDelete={this.deleteAnnotation} onExport={this.exportAnnotation} onView={this.viewBookmark} selected={ this.state.selection.find( item => item.annotationId === annotation.annotationId ) !== undefined } onSelect={this.selectItem} showSub={annotation.annotationId in this.state.showSub} toggleSub={this.toggleSub} /> )) : <h3>∅ No results</h3> } </div> </div> ) } render() { let detailsModal = null; if(this.state.detailBookmark) { detailsModal = ( <ResourceViewerModal bookmark={this.state.detailBookmark} onClose={this.closeItemDetails}/> ) } return ( <div className={classNames(IDUtil.cssClassName('annotation-table'),{loading:this.state.loading})}> <NestedTable items={this.state.annotations} selection={this.state.selection} sortItems={this.sortAnnotations} orders={this.orders} filterItems={this.filterAnnotations} filters={this.state.filters} renderResults={this.renderResults} onExport={this.exportAnnotations} showSub={this.state.showSub} uid={this.props.project.id + "-annotation-"+this.props.type} /> <BulkActions bulkActions={this.bulkActions} selection={this.state.selection} /> {detailsModal} </div> ) } } AnnotationTable.propTypes = { api: PropTypes.object.isRequired, filters: PropTypes.array.isRequired, loadBookmarkCount: PropTypes.func.isRequired, sort: PropTypes.array, title: PropTypes.string, type: PropTypes.string, user: PropTypes.object.isRequired, }; AnnotationTable.defaultTypes = { filters: [], sort: [], }; export default AnnotationTable;