import _, {noop} from 'lodash'; import React from 'react'; import classNames from 'classnames'; import {Item} from './index'; import {isCheckAllowed, closeActionsMenu, bindMarkItemShortcut} from '../helpers'; import {isMediaEditable} from 'core/config'; import {gettext, IScopeApply} from 'core/utils'; import {IArticle} from 'superdesk-api'; import {AuthoringWorkspaceService} from 'apps/authoring/authoring/services/AuthoringWorkspaceService'; import ng from 'core/services/ng'; import {IMultiSelectOptions} from 'core/MultiSelectHoc'; import {IActivityService} from 'core/activity/activity'; import {isButtonClicked} from './Item'; import {querySelectorParent} from 'core/helpers/dom/querySelectorParent'; import {IRelatedEntities} from 'core/getRelatedEntities'; import {OrderedMap} from 'immutable'; import {MultiSelect} from 'core/ArticlesListV2MultiSelect'; interface IProps { itemsList: Array; itemsById: any; relatedEntities: IRelatedEntities; narrow: boolean; view: 'compact' | 'mgrid' | 'photogrid'; selected: string; swimlane: any; profilesById: any; highlightsById: any; markedDesksById: any; desksById: any; ingestProvidersById: any; usersById: any; onMonitoringItemSelect: any; onMonitoringItemDoubleClick: any; singleLine: any; customRender: any; flags: { hideActions: any; }; hideActionsForMonitoringItems: boolean; groupId: any; viewColumn: any; loading: any; scopeApply: IScopeApply; scopeApplyAsync: IScopeApply; edit(item: IArticle): void; preview(item: IArticle): void; multiSelect?: IMultiSelectNew | ILegacyMultiSelect; } export interface ILegacyMultiSelect { kind: 'legacy'; multiSelect(item: IArticle, selected: boolean, multiSelectMode: boolean): void; setSelectedItem(itemId: string): void; } export interface IMultiSelectNew { kind: 'new'; options: IMultiSelectOptions; items: OrderedMap; MultiSelectComponent: typeof MultiSelect; } interface IState { bindedShortcuts: Array; actioning: {}; } /** * Item list component */ export class ItemList extends React.Component { updateTimeout: any; selectedCom: any; angularservices: { $rootScope: any; $timeout: any; activityService: IActivityService; archiveService: any; authoringWorkspace: AuthoringWorkspaceService; keyboardManager: any; Keys: any; monitoringState: any; multi: any; search: any; storage: any; superdesk: any; workflowService: any; }; focusableElement: HTMLUListElement | null; constructor(props) { super(props); this.state = { bindedShortcuts: [], actioning: {}, }; this.select = this.select.bind(this); this.selectItem = this.selectItem.bind(this); this.dbClick = this.dbClick.bind(this); this.edit = this.edit.bind(this); this.deselectAll = this.deselectAll.bind(this); this.setSelectedItem = this.setSelectedItem.bind(this); this.getSelectedItem = this.getSelectedItem.bind(this); this.handleKey = this.handleKey.bind(this); this.setSelectedComponent = this.setSelectedComponent.bind(this); this.modifiedUserName = this.modifiedUserName.bind(this); this.multiSelectCurrentItem = this.multiSelectCurrentItem.bind(this); this.bindActionKeyShortcuts = this.bindActionKeyShortcuts.bind(this); this.unbindActionKeyShortcuts = this.unbindActionKeyShortcuts.bind(this); this.angularservices = { $rootScope: ng.get('$rootScope'), $timeout: ng.get('$timeout'), activityService: ng.get('activityService'), archiveService: ng.get('archiveService'), authoringWorkspace: ng.get('authoringWorkspace'), keyboardManager: ng.get('keyboardManager'), Keys: ng.get('Keys'), monitoringState: ng.get('monitoringState'), multi: ng.get('multi'), search: ng.get('search'), storage: ng.get('storage'), superdesk: ng.get('superdesk'), workflowService: ng.get('workflowService'), }; } // Method to check the selectBox of the selected item multiSelectCurrentItem() { if (this.props.multiSelect.kind !== 'legacy') { throw new Error('Legacy multiselect API expected.'); } const selectedItem = this.getSelectedItem(); if (selectedItem) { this.props.multiSelect.multiSelect(selectedItem, !selectedItem.selected, false); } } select(item: IArticle, event) { // Don't select item / open preview when a button is clicked. // The button can be three dots menu, bulk actions checkbox, a button to preview existing highlights etc. if (isButtonClicked(event)) { return; } if (typeof this.props.onMonitoringItemSelect === 'function') { this.props.onMonitoringItemSelect(item, event); return; } const {$timeout} = this.angularservices; this.setSelectedItem(item); if (event && event.ctrlKey) { return this.selectItem(item); } $timeout.cancel(this.updateTimeout); if (item && this.props.preview != null) { this.props.scopeApply(() => { this.props.preview(item); this.bindActionKeyShortcuts(item); }); } } /* * Unbind all item actions */ unbindActionKeyShortcuts(callback?) { const {keyboardManager} = this.angularservices; this.state.bindedShortcuts.forEach((shortcut) => { keyboardManager.unbind(shortcut); }); this.setState({bindedShortcuts: []}, callback); } /* * Bind item actions on keyboard shortcuts * Keyboard shortcuts are defined with actions * * @param {Object} item */ bindActionKeyShortcuts(selectedItem) { const { activityService, archiveService, keyboardManager, superdesk, workflowService, } = this.angularservices; const doBind = () => { const intent = {action: 'list', type: archiveService.getType(selectedItem)}; superdesk.findActivities(intent, selectedItem).forEach((activity) => { if (activity.keyboardShortcut && workflowService.isActionAllowed(selectedItem, activity.action)) { this.state.bindedShortcuts.push(activity.keyboardShortcut); keyboardManager.bind(activity.keyboardShortcut, () => { if (_.includes(['mark.item', 'mark.desk'], activity._id)) { bindMarkItemShortcut(activity.label); } else { activityService.start(activity, {data: {item: selectedItem}}); } }); } }); }; // First unbind all binded shortcuts if (this.state.bindedShortcuts.length) { this.unbindActionKeyShortcuts(() => { doBind(); }); } else { doBind(); } } selectItem(item) { if (this.props.multiSelect.kind !== 'legacy') { throw new Error('Legacy multiselect API expected.'); } if (isCheckAllowed(item)) { const selected = !item.selected; this.props.multiSelect.multiSelect(item, selected, false); } } setActioning(item: IArticle, isActioning: boolean) { const {search} = this.angularservices; const actioning = Object.assign({}, this.state.actioning); const itemId = search.generateTrackByIdentifier(item); actioning[itemId] = isActioning; this.setState({actioning}); } dbClick(item) { if (typeof this.props.onMonitoringItemDoubleClick === 'function') { this.props.onMonitoringItemDoubleClick(item); return; } const {superdesk, $timeout} = this.angularservices; const {authoringWorkspace} = this.angularservices; const activities = superdesk.findActivities({action: 'list', type: item._type}, item); const canEdit = _.reduce(activities, (result, value) => result || value._id === 'edit.item', false); this.setSelectedItem(item); $timeout.cancel(this.updateTimeout); if (this.props.flags?.hideActions) { return; } if (item._type === 'externalsource') { if (!isMediaEditable(item)) { return; } this.setActioning(item, true); superdesk.intent('list', 'externalsource', {item: item}, 'fetch-externalsource') .then((archiveItem) => { archiveItem.guid = archiveItem._id; // fix item guid to match new item _id this.props.scopeApplyAsync(() => { if (this.props.edit != null) { this.props.edit(archiveItem); } else { authoringWorkspace.open(archiveItem); } }); }) .finally(() => { this.setActioning(item, false); }); } else if (canEdit && this.props.edit != null) { this.props.scopeApply(() => { this.props.edit(item); }); } else { this.props.scopeApply(() => { authoringWorkspace.open(item); }); } } edit(item: IArticle, event) { const {authoringWorkspace} = this.angularservices; const {$timeout} = this.angularservices; if (this.props.selected !== item._id) { this.select(item, event); } $timeout.cancel(this.updateTimeout); if (this.props.flags?.hideActions || item == null) { return; } if (this.props.edit != null) { this.props.scopeApply(() => { this.props.edit(item); }); } else { this.props.scopeApply(() => { authoringWorkspace.open(item); }); } } deselectAll() { if (this.props.multiSelect.kind !== 'legacy') { throw new Error('Legacy multiselect API expected.'); } this.props.multiSelect.setSelectedItem(null); this.unbindActionKeyShortcuts(); } setSelectedItem(item: IArticle) { if (this.props.multiSelect.kind !== 'legacy') { throw new Error('Legacy multiselect API expected.'); } const {monitoringState, $rootScope, search} = this.angularservices; if (monitoringState.state.activeGroup !== this.props.groupId) { // If selected item is from another group, deselect all $rootScope.$broadcast('item:unselect'); monitoringState.setState({activeGroup: this.props.groupId}); } this.props.multiSelect.setSelectedItem(item ? search.generateTrackByIdentifier(item) : null); } getSelectedItem() { const selected = this.props.selected; return this.props.itemsById[selected]; } handleKey(event) { if (querySelectorParent(event.target, 'button', {self: true}) != null) { // don't execute key bindings when a button inside the list item is focused. return; } // don't do anything when modifier key is pressed // this allows shortcuts defined in activities to work without two actions firing for one shortcut if (event.ctrlKey || event.altKey || event.shiftKey) { return; } const {Keys, monitoringState} = this.angularservices; const KEY_CODES = Object.freeze({ X: 'X'.charCodeAt(0), }); let diff; const moveActiveGroup = (_event) => { _event.preventDefault(); _event.stopPropagation(); this.deselectAll(); // deselect active item const keyCode = _event.keyCode; this.props.scopeApplyAsync(() => { monitoringState.moveActiveGroup(keyCode === Keys.pageup ? -1 : 1); }); }; const openItem = (_event) => { if (this.props.selected) { this.edit(this.getSelectedItem(), _event); } event.stopPropagation(); }; const performMultiSelect = () => { event.preventDefault(); event.stopPropagation(); this.multiSelectCurrentItem(); }; switch (event.keyCode) { case Keys.right: case Keys.down: diff = 1; closeActionsMenu(); break; case Keys.left: case Keys.up: diff = -1; closeActionsMenu(); break; case Keys.enter: openItem(event); closeActionsMenu(); break; case Keys.pageup: case Keys.pagedown: moveActiveGroup(event); closeActionsMenu(); break; case KEY_CODES.X: performMultiSelect(); closeActionsMenu(); break; } if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { const nextEl = document.activeElement.nextElementSibling; if (nextEl instanceof HTMLElement) { // Don't scroll the list. The list will be scrolled automatically // when an item is focued that is outside of the viewport. event.preventDefault(); nextEl.focus(); } } if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { const prevEl = document.activeElement.previousElementSibling; if (prevEl instanceof HTMLElement) { // Don't scroll the list. The list will be scrolled automatically // when an item is focued that is outside of the viewport. event.preventDefault(); prevEl.focus(); } } } componentWillUnmount() { this.unbindActionKeyShortcuts(); closeActionsMenu(); } setSelectedComponent(com) { this.selectedCom = com; } modifiedUserName(versionCreator) { return this.props.usersById[versionCreator] ? this.props.usersById[versionCreator].display_name : null; } focus() { if (this.focusableElement == null) { return; } // Focus only if a child item doesn't already have focus. // Otherwise, it always re-focuses the entire list after clicking a particular item // and user is unable to use keyboard shortcuts on an item that was clicked. if (this.focusableElement.contains(document.activeElement) === false) { this.focusableElement.focus(); } } render() { const {storage} = this.angularservices; const isEmpty = !this.props.itemsList.length; if (this.props.loading) { return (
    { this.focusableElement = el; }} data-test-id="item-list--loading" >
  • {gettext('Loading...')}
); } else if (isEmpty) { return (
    { this.focusableElement = el; }} >
  • {gettext('There are currently no items')}
); } return (
    { this.handleKey(event); }} tabIndex={-1} ref={(el) => { this.focusableElement = el; }} > { this.props.itemsList.map((itemId) => { const item = this.props.itemsById[itemId]; const task = item.task || {desk: null}; return ( ); }) }
); } }