labo-components
Version:
696 lines (640 loc) • 24.9 kB
JSX
import AnnotationAPI from '../../../../api/AnnotationAPI';
import IDUtil from '../../../../util/IDUtil';
import TimeUtil from '../../../../util/TimeUtil';
import { CUSTOM } from "../../../../util/AnnotationConstants";
import { exportDataAsJSON } from '../../helpers/Export';
import BulkActions from '../../helpers/BulkActions';
import {
createAnnotationOptionList,
createOptionList,
createClassificationOptionList,
createSimpleArrayOptionList
} from '../../helpers/OptionList';
import ResourceViewerModal from '../../ResourceViewerModal';
import FlexRouter from '../../../../util/FlexRouter';
import BookmarkRow from './BookmarkRow';
import NestedTable from '../../helpers/NestedTable';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import CollectionUtil from '../../../../util/CollectionUtil';
import LocalStorageHandler from '../../../../util/LocalStorageHandler';
import Loading from '../../../shared/Loading';
/**
* This view handles the loading, filtering and selection of data of
* the Bookmarks list of a project. It is displayed using the NestedTable component.
*/
class BookmarkTable extends React.PureComponent {
constructor(props) {
super(props);
this.orders = [
{ value: 'created', name: 'Bookmark created' },
{ value: 'newest', name: 'Newest objects first' },
{ value: 'oldest', name: 'Oldest objects first' },
{ value: 'name-az', name: 'Title A-Z' },
{ value: 'name-za', name: 'Title Z-A' },
{ value: 'mediatype', name: 'Media' },
{ value: 'playable', name: 'Playable' },
{ value: 'dataset', name: 'Dataset' },
{ value: 'group', name: 'Groups' }
];
this.bulkActions = [
{ title: 'Delete', onApply: this.deleteBookmarks.bind(this) },
{ title: 'Export', onApply: this.exportBookmarks.bind(this) }
];
this.state = {
annotations: [], //FIXME no longer filled with the new API call!!
bookmarks: [],
selection: [],
subMediaObject: {},
subSegment: {},
loading: true,
detailBookmark: null,
filters: []
};
// bind functions (TODO get rid of these, unnecessary and confusing)
this.viewBookmark = this.viewBookmark.bind(this);
this.deleteBookmarks = this.deleteBookmarks.bind(this);
this.deleteBookmark = this.deleteBookmark.bind(this);
this.filterBookmarks = this.filterBookmarks.bind(this);
this.sortBookmarks = this.sortBookmarks.bind(this);
this.renderResults = this.renderResults.bind(this);
this.selectAllChange = this.selectAllChange.bind(this);
this.selectItem = this.selectItem.bind(this);
this.refreshBookmarks = this.refreshBookmarks.bind(this);
this.toggleSubMediaObject = this.toggleSubMediaObject.bind(this);
this.toggleSubSegment = this.toggleSubSegment.bind(this);
this.unFoldAll = this.unFoldAll.bind(this);
this.foldAll = this.foldAll.bind(this);
}
componentDidMount() {
this.loadBookmarks();
}
loadBookmarks() {
this.setState({
loading: true
});
AnnotationAPI.getBookmarks(
this.props.user.id,
this.props.project.id,
this.onLoadResourceList.bind(this)
);
}
//The resource list now also contains the data of the resources
onLoadResourceList(bookmarks) {
this.setState({
bookmarks: bookmarks,
loading: false,
filters: this.getFilters(bookmarks)
});
this.updateSelection(bookmarks);
}
//Get filter object
getFilters(items) {
return [
// search filter
{
title: '',
key: 'keywords',
type: 'search',
placeholder: 'Search Bookmarks'
},
// type filter
{
title: 'Media',
key: 'mediaType',
type: 'select',
options: createSimpleArrayOptionList(
items,
i => i.object.mediaTypes
)
},
// group filter
{
title: 'Group',
key: 'group',
type: 'select',
titleAttr: 'Bookmark group',
options: createClassificationOptionList(items, 'groups')
},
// annotations filter
{
title: 'Annotations',
key: 'annotations',
type: 'select',
titleAttr: 'MediaObject annotations',
options: [
{ value: 'yes', name: 'With annotations' },
{ value: 'no', name: 'Without annotations' },
{ value: '', name: '-----------', disabled: true }
].concat(createAnnotationOptionList(items))
},
// segment filter
{
title: 'Segments',
key: 'segments',
type: 'select',
options: [
{ value: 'yes', name: 'Yes' },
{ value: 'no', name: 'No' }
]
}
];
}
//Update Selection list, based on available items
updateSelection(items) {
this.setState({
selection: items.filter(item =>
this.state.selection.some(i => i.resourceId === item.resourceId)
)
});
}
filterBookmarks(bookmarks, filter) {
// filter on type
if (filter.mediaType) {
bookmarks = bookmarks.filter(
bookmark =>
bookmark.object &&
bookmark.object.mediaTypes &&
bookmark.object.mediaTypes.includes(filter.mediaType)
);
}
// filter on group
if (filter.group) {
bookmarks = bookmarks.filter(
bookmark =>
bookmark.groups &&
bookmark.groups.some(g => g.annotationId === filter.group)
);
}
// filter on annotations
if (filter.annotations) {
switch (filter.annotations) {
case 'yes':
bookmarks = bookmarks.filter(
bookmark => bookmark.annotations.filter((annotation)=>(annotation.annotationType !== CUSTOM)).length > 0
);
break;
case 'no':
bookmarks = bookmarks.filter(
bookmark => bookmark.annotations.filter((annotation)=>(annotation.annotationType !== CUSTOM)).length === 0
);
break;
default:
bookmarks = bookmarks.filter(bookmark =>
bookmark.annotations.filter((annotation)=>(annotation.annotationType !== CUSTOM)).some(
a => a.annotationType === filter.annotations
)
);
}
}
// filter on segments
if (filter.segments) {
switch (filter.segments) {
case 'yes':
bookmarks = bookmarks.filter(
bookmark => bookmark.segments.length > 0
);
break;
case 'no':
bookmarks = bookmarks.filter(
bookmark => bookmark.segments.length === 0
);
break;
}
}
// filter on keywords in title, dataset or type
if (filter.keywords) {
const keywords = filter.keywords.split(' ');
keywords.forEach(k => {
k = k.toLowerCase();
bookmarks = bookmarks.filter(
bookmark =>
// object
(bookmark.object &&
Object.keys(bookmark.object).some(
key =>
typeof bookmark.object[key] === 'string' &&
bookmark.object[key]
.toLowerCase()
.includes(k)
)) ||
// annotations
(bookmark.annotations &&
bookmark.annotations.some(annotation =>
Object.keys(annotation).some(
key =>
typeof annotation[key] === 'string' &&
annotation[key]
.toLowerCase()
.includes(k)
)
))
);
});
}
return bookmarks;
}
sortBookmarks(bookmarks, field) {
if (!bookmarks) {
return [];
}
let collectionClassesList = {};
const sorted = bookmarks.map(item => {
if(!collectionClassesList[item.object.dataset]) {
collectionClassesList[item.object.dataset] = CollectionUtil.getCollectionClass(
this.props.user.id,
this.props.user.name,
item.object.dataset,
true
);
}
if (collectionClassesList[item.object.dataset]) {
const formattedDates = item.object.date
? TimeUtil.sortedFormattedDates(
collectionClassesList[item.object.dataset].prototype.getFormattedDates(
item.object.date
)
)
: null;
// Enhance bookmarks with a timestamp to allow sorting.
item.object.sortingDate = TimeUtil.formattedDatesToLowestTimestamp(
formattedDates
);
// Enhance bookmarks with a formatted string/array of dates.
item.object.formattedDate = formattedDates;
}
return item;
});
const getFirst = (a, empty) => (a && a.length > 0 ? a[0] : empty);
switch (field) {
case 'created':
sorted.sort((a,b) => {
return (b.object.sortingDate != null) - (a.object.sortingDate != null) || a.object.sortingDate - b.object.sortingDate;
});
break;
case 'newest':
sorted.sort(
(a, b) =>
(a.object.sortingDate === null) -
(b.object.sortingDate === null) ||
+(a.object.sortingDate < b.object.sortingDate) ||
-(a.object.sortingDate > b.object.sortingDate)
);
break;
case 'oldest':
sorted.sort(
(a, b) =>
(a.object.sortingDate === null) -
(b.object.sortingDate === null) ||
+(a.object.sortingDate > b.object.sortingDate) ||
-(a.object.sortingDate < b.object.sortingDate)
);
break;
case 'name-az':
sorted.sort((a, b) =>
a.object.title > b.object.title ? 1 : -1
);
break;
case 'name-za':
sorted.sort((a, b) =>
a.object.title < b.object.title ? 1 : -1
);
break;
case 'mediatype': {
// '~' > move empty to bottom
const e = '~';
sorted.sort((a, b) =>
getFirst(a.object.mediaTypes, e) >
getFirst(b.object.mediaTypes, e)
? 1
: -1
);
break;
}
case 'playable':
sorted.sort((a, b) =>
a.object.playable < b.object.playable ? -1 : 1
);
break;
case 'dataset':
sorted.sort((a, b) =>
a.object.dataset > b.object.dataset ? 1 : -1
);
break;
case 'group': {
// '~' > move empty to bottom
const e = { label: '~' };
sorted.sort((a, b) =>
getFirst(a.groups, e).label > getFirst(b.groups, e).label
? 1
: -1
);
break;
}
default:
return sorted;
}
return sorted;
}
//delete multiple bookmarks
deleteBookmarks(bookmarks) {
if (bookmarks) {
if (
!confirm(
'Are you sure you want to remove the selected bookmarks and all its annotations?'
)
) {
return;
}
//populate the deletion list required for the annotation API
const deletionList = [];
bookmarks.forEach(b => {
b.targetObjects.forEach(targetObject => {
deletionList.push({
annotationId: targetObject.parentAnnotationId,
type: 'target',
partId: targetObject.assetId
});
});
});
//now delete the whole selection in a single call to the API
AnnotationAPI.deleteUserAnnotations(
this.props.user.id,
deletionList,
success => {
setTimeout(() => {
// load new data
this.loadBookmarks();
// update bookmark count in project menu
this.props.loadBookmarkCount();
}, 500);
}
);
}
}
exportBookmarks(selection) {
const data = this.state.bookmarks.filter(item =>
selection.some(i => i.resourceId === item.resourceId)
);
exportDataAsJSON(data);
}
deleteBookmark(bookmark) {
this.deleteBookmarks([bookmark]);
}
makeActiveProject() {
LocalStorageHandler.storeJSONInLocalStorage(
'stored-active-project',
this.props.project
);
}
viewBookmark(bookmark) {
// make current project active
if (bookmark) {
this.makeActiveProject();
}
this.setState({
detailBookmark: bookmark
});
}
// opens a new popup window for the specific resourceId
openResourceViewer = (item, unique) => {
const resource = { index: item.index, resourceId: item.resourceId, startTime: item.startTime };
FlexRouter.popupResourceViewer(
'/tool/resource-viewer',
resource,
null,
'bookmarks-table-resource-viewer-popup' +
(unique ? item.resourceId : ''),
true,
this.refreshBookmarks
);
};
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.resourceId === item.resourceId
);
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
});
}
selectItem(item, select) {
let newSelection = this.state.selection.slice(); //copy the array
const index = newSelection.findIndex(selected => {
return selected.resourceId === item.resourceId;
});
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
});
}
// Refresh the bookmark data (assuming changes have been made)
refreshBookmarks = () => {
// set viewbookmark to null
this.viewBookmark(null);
// update bookmark count in project menu
this.props.loadBookmarkCount();
// refresh data
this.loadBookmarks();
};
// Toggle sublevel mediaobject visibility
toggleSubMediaObject(id) {
const subMediaObject = Object.assign({}, this.state.subMediaObject);
if (id in subMediaObject) {
delete subMediaObject[id];
} else {
subMediaObject[id] = true;
}
// remove from subSegments
const subSegment = Object.assign({}, this.state.subSegment);
delete subSegment[id];
this.setState({ subMediaObject, subSegment });
}
// Toggle sublevel segment visibility
toggleSubSegment(id) {
const subSegment = Object.assign({}, this.state.subSegment);
if (id in subSegment) {
delete subSegment[id];
} else {
subSegment[id] = true;
}
// remove from subMediaObject
const subMediaObject = Object.assign({}, this.state.subMediaObject);
delete subMediaObject[id];
this.setState({ subMediaObject, subSegment });
}
unFoldAll() {
const showSub = {};
switch (this.foldTarget.value) {
case 'mediaobject':
this.state.bookmarks.forEach(b => {
if (b.annotations && b.annotations.length > 0) {
showSub[b.resourceId] = true;
}
});
this.setState({ subSegment: {}, subMediaObject: showSub });
break;
case 'segments':
this.state.bookmarks.forEach(b => {
if (b.segments && b.segments.length > 0) {
showSub[b.resourceId] = true;
}
});
this.setState({ subMediaObject: {}, subSegment: showSub });
break;
}
}
foldAll() {
switch (this.foldTarget.value) {
case 'mediaobject':
this.setState({ subMediaObject: {} });
break;
case 'segments':
this.setState({ subSegment: {} });
break;
}
}
renderResults(renderState) {
const annotationTypeFilter =
renderState.filter.annotations &&
!['yes', 'no'].includes(renderState.filter.annotations)
? renderState.filter.annotations
: '';
return (
<div>
<h2>
<input
type="checkbox"
checked={
renderState.visibleItems.length > 0 &&
renderState.visibleItems.every(
item =>
item &&
this.state.selection.some(
i => i.resourceId === item.resourceId
)
)
}
onChange={this.selectAllChange.bind(
this,
renderState.visibleItems
)}
/>
Bookmarks:{' '}
<span className="count">
{renderState.visibleItems.length || 0}
</span>
<div className="fold">
<div className="filter">
<span onClick={this.unFoldAll}>Show all</span>
/
<span onClick={this.foldAll}>Hide all</span>
</div>
<select ref={elem => (this.foldTarget = elem)}>
<option value="mediaobject">
MediaObject annotations
</option>
<option value="segments">Segments</option>
</select>
</div>
</h2>
<div className="bookmark-table">
{renderState.visibleItems.length ? (
renderState.visibleItems.map((bookmark, index) => (
<BookmarkRow
key={bookmark.resourceId}
bookmark={bookmark}
onDelete={this.deleteBookmark}
onExport={exportDataAsJSON}
openResourceViewer={this.openResourceViewer}
selected={
this.state.selection.find(
item =>
item.resourceId ===
bookmark.resourceId
) !== undefined
}
onSelect={this.selectItem}
showSubMediaObject={
bookmark.resourceId in
this.state.subMediaObject
}
showSubSegment={
bookmark.resourceId in this.state.subSegment
}
toggleSubMediaObject={this.toggleSubMediaObject}
toggleSubSegment={this.toggleSubSegment}
annotationTypeFilter={annotationTypeFilter}
projectId={this.props.project.id}
/>
))
) : (
<h3>∅ No results</h3>
)}
</div>
</div>
);
}
render() {
if (this.state.loading) {//FIXME the spinner is hidden behind the other content (z-index does nog work)
return <Loading message="Loading bookmarks..."/>
} else {
let detailsModal = null;
if (this.state.detailBookmark) {
detailsModal = (
<ResourceViewerModal
bookmark={this.state.detailBookmark}
onClose={this.refreshBookmarks}
/>
);
}
return (
<div className={IDUtil.cssClassName('bookmark-table')}>
<NestedTable
uid={this.props.project.id + '-bookmarks'}
filterItems={this.filterBookmarks}
renderResults={this.renderResults}
onExport={exportDataAsJSON}
items={this.state.bookmarks}
sortItems={this.sortBookmarks}
selection={this.state.selection}
orders={this.orders}
filters={this.state.filters}
toggleSubMediaObject={this.state.subMediaObject}
toggleSubSegment={this.state.subSegment}
/>
<BulkActions
bulkActions={this.bulkActions}
selection={this.state.selection}
/>
{detailsModal}
</div>
);
}
}
}
BookmarkTable.propTypes = {
user: PropTypes.object.isRequired,
project: PropTypes.object.isRequired,
loadBookmarkCount: PropTypes.func.isRequired,
openResourceViewer: PropTypes.func.isRequired,
viewBookmark: PropTypes.func.isRequired
};
export default BookmarkTable;