/* eslint-disable react/no-multi-comp */ import React from 'react'; import classNames from 'classnames'; import {get} from 'lodash'; import {ActionsMenu} from './actions-menu/ActionsMenu'; import {closeActionsMenu, isIPublishedArticle} from '../helpers'; import {gettext} from 'core/utils'; import {ItemSwimlane} from './ItemSwimlane'; import {ItemPhotoGrid} from './ItemPhotoGrid'; import {ListItemTemplate} from './ItemListTemplate'; import {ItemMgridTemplate} from './ItemMgridTemplate'; import {IArticle, IDesk, IPublishedArticle} from 'superdesk-api'; import {querySelectorParent} from 'core/helpers/dom/querySelectorParent'; import {AuthoringWorkspaceService} from 'apps/authoring/authoring/services/AuthoringWorkspaceService'; import {httpRequestJsonLocal} from 'core/helpers/network'; import {appConfig} from 'appConfig'; import ng from 'core/services/ng'; import {IScopeApply} from 'core/utils'; import {ILegacyMultiSelect, IMultiSelectNew} from './ItemList'; import {IActivityService} from 'core/activity/activity'; import {IActivity} from 'superdesk-interfaces/Activity'; import {IRelatedEntities} from 'core/getRelatedEntities'; export function isButtonClicked(event): boolean { // don't trigger the action if a button inside a list view is clicked // if an extension registers a button, it should be able to totally control it. // target can be an image or an icon inside a button, so parents need to be checked too return querySelectorParent(event.target, 'button', {self: true}) != null; } const CLICK_TIMEOUT = 300; const actionsMenuDefaultTemplate = (toggle, stopEvent) => (
); interface IProps { swimlane: any; item: IArticle | IPublishedArticle; relatedEntities: IRelatedEntities; profilesById: any; highlightsById: any; markedDesksById: any; ingestProvider: any; versioncreator: any; multiSelect: IMultiSelectNew | ILegacyMultiSelect; desk: IDesk; flags: any; view: any; onDbClick: any; onEdit: any; onSelect(item: IArticle, event): void; narrow: any; hideActions: boolean; multiSelectDisabled: boolean; isNested: boolean; actioning: boolean; singleLine: any; customRender: any; scopeApply: IScopeApply; } interface IState { hover: boolean; actioning: boolean; isActionMenuOpen: boolean; showNested: boolean; loading: boolean; nested: Array; } export class Item extends React.Component { private clickTimeout: number; private _mounted: boolean; private mouseDown: boolean; constructor(props) { super(props); this.state = { hover: false, actioning: false, isActionMenuOpen: false, showNested: false, loading: false, nested: [], }; this.handleClick = this.handleClick.bind(this); this.edit = this.edit.bind(this); this.handleDoubleClick = this.handleDoubleClick.bind(this); this.setActioningState = this.setActioningState.bind(this); this.setHoverState = this.setHoverState.bind(this); this.unsetHoverState = this.unsetHoverState.bind(this); this.onDragStart = this.onDragStart.bind(this); this.openAuthoringView = this.openAuthoringView.bind(this); this.toggleNested = this.toggleNested.bind(this); } componentWillMount() { if (appConfig?.apps?.includes('superdesk-planning')) { this.loadPlanningModals(); } } componentDidMount() { this._mounted = true; } componentWillUnmount() { this._mounted = false; closeActionsMenu(this.props.item._id); } componentWillReceiveProps(nextProps) { if (nextProps.item !== this.props.item) { closeActionsMenu(this.props.item._id); } this.setActioningState(nextProps.actioning); } loadPlanningModals() { const session = ng.get('session'); const superdesk = ng.get('superdesk'); const activityService: IActivityService = ng.get('activityService'); if (!['add_to_planning', 'fulfil_assignment'].includes(get(this.props, 'item.lock_action')) || get(this.props, 'item.lock_user') !== session.identity._id || get(this.props, 'item.lock_session') !== session.sessionId) { return; } let planningActivity: IActivity | null; const activities = superdesk.findActivities({action: 'list', type: 'archive'}, this.props.item); // Planning: if page probably was refreshed / loaded during the add-to-planning operation, // reload the add-to-planning modal if (this.props.item.lock_action === 'add_to_planning') { planningActivity = activities.find((a) => a._id === 'planning.addto'); } else if (this.props.item.lock_action === 'fulfil_assignment') { planningActivity = activities.find((a) => a._id === 'planning.fulfil'); } const openActivities = activityService.activityStack || []; // Item list is rerendered from planning on certain actions and this will trigger // opening a modal that might be open already (without page refresh) if (openActivities.find(({activity}) => activity._id === planningActivity._id) != null) { // if activity is open already, don't do anything return; } if (planningActivity) { this.setActioningState(true); activityService.start(planningActivity, {data: {item: this.props.item}}) .finally(() => { if (this._mounted) { this.setActioningState(false); } }); } } shouldComponentUpdate(nextProps: IProps, nextState) { return nextProps.swimlane !== this.props.swimlane || nextProps.item !== this.props.item || nextProps.view !== this.props.view || nextProps.flags.selected !== this.props.flags.selected || nextProps.narrow !== this.props.narrow || nextProps.actioning !== this.props.actioning || nextProps.multiSelect !== this.props.multiSelect || nextState !== this.state; } handleClick(event) { if (isButtonClicked(event)) { return; } if (!this.props.item.gone && !this.clickTimeout) { event.persist(); // make event available in timeout callback this.clickTimeout = window.setTimeout(() => { this.clickTimeout = null; this.props.onSelect(this.props.item, event); }, CLICK_TIMEOUT); } } /** * Opens the item in authoring in view mode * @param {string} itemId Id of the document */ openAuthoringView(itemId) { const authoringWorkspace: AuthoringWorkspaceService = ng.get('authoringWorkspace'); authoringWorkspace.edit({_id: itemId}, 'view'); } edit(event) { if (!this.props.item.gone) { this.props.onEdit(this.props.item); } } handleDoubleClick(event) { if (isButtonClicked(event)) { return; } if (this.clickTimeout) { window.clearTimeout(this.clickTimeout); this.clickTimeout = null; } if (!this.props.item.gone) { this.props.onDbClick(this.props.item); } } /** * Set Actioning state * @param {Boolean} isActioning - true if activity is in-progress, and false if completed */ setActioningState(isActioning) { this.setState({actioning: isActioning}); } setHoverState() { if (this.state.hover !== true) { this.setState({hover: true}); } } unsetHoverState() { this.setState({hover: false}); } onDragStart(event) { ng.get('dragitem').start(event, this.props.item); } toggleNested(event) { event.stopPropagation(); const showNested = !this.state.showNested; if (showNested && isIPublishedArticle(this.props.item) && !this.state.loading && !this.state.nested.length) { this.setState({loading: true}); this.fetchNested(this.props.item); } this.setState({showNested: !this.state.showNested}); } fetchNested(item: IPublishedArticle) { httpRequestJsonLocal<{_items: Array}>({ method: 'GET', path: '/published', urlParams: {source: JSON.stringify({ query: { bool: { must: {term: {family_id: item.archive_item.family_id}}, must_not: {term: {_id: item.item_id}}, }, }, sort: [{'versioncreated': 'desc'}], })}, }).then((data) => { this.setState({loading: false, nested: data._items}); }).catch(() => { this.setState({loading: false}); }); } render() { const {item} = this.props; let classes = this.props.view === 'photogrid' ? 'sd-grid-item sd-grid-item--with-click' : 'media-box media-' + item.type; const selectedInSingleSelectMode = this.props.flags.selected; const selectedInMultiSelectMode = this.props.item.selected; const itemSelected = selectedInSingleSelectMode || selectedInMultiSelectMode; // Customize item class from its props if (this.props.customRender && typeof this.props.customRender.getItemClass === 'function') { classes = `${classes} ${this.props.customRender.getItemClass(item)}`; } const isLocked: boolean = (item.lock_user && item.lock_session) != null; const getActionsMenu = (template = actionsMenuDefaultTemplate) => this.props.hideActions !== true && (this.state.hover || itemSelected) && !item.gone ? ( ) : null; const getTemplate = () => { switch (this.props.view) { case 'swimlane2': return ( ); case 'mgrid': return ( ); case 'photogrid': return ( ); default: return ( ); } }; const getNested = () => { switch (this.props.view) { case 'swimlane2': case 'mgrid': case 'photogrid': return null; default: if (!this.state.nested.length) { return null; } return (
{this.state.nested.map((childItem) => ( null} multiSelectDisabled={false} swimlane={this.props.swimlane} highlightsById={this.props.highlightsById} markedDesksById={this.props.markedDesksById} ingestProvider={this.props.ingestProvider} desk={this.props.desk} view={this.props.view} versioncreator={this.props.versioncreator} onEdit={this.props.onEdit} onDbClick={this.props.onDbClick} multiSelect={this.props.multiSelect} actioning={false} singleLine={this.props.singleLine} customRender={this.props.customRender} scopeApply={this.props.scopeApply} /> ))}
); } }; // avoid any actions on nested items const getCallback = (func) => this.props.isNested ? (event) => event.stopPropagation() : func; return React.createElement( this.props.isNested ? 'div' : 'li', { id: item._id, key: item._id, className: classNames( 'list-item-view', { 'actions-visible': this.props.hideActions !== true, 'active': itemSelected, 'selected': this.props.item.selected && !this.props.flags.selected, 'sd-list-item-nested': this.state.nested.length, 'sd-list-item-nested--expanded': this.state.nested.length && this.state.showNested, 'sd-list-item-nested--collapsed': this.state.nested.length && this.state.showNested === false, }, ), onMouseOver: getCallback(this.setHoverState), onMouseLeave: getCallback(this.unsetHoverState), onDragStart: getCallback(this.onDragStart), onFocus: getCallback((event) => { // Only open preview on focus when it is triggered via keyboard. // When mouse is used, preview will open on click. if (this.mouseDown !== true) { // not using this.select in order to avoid the timeout // that is used to enable double-click if (!this.props.item.gone) { this.props.onSelect(this.props.item, event); } } }), onMouseDown: () => { this.mouseDown = true; }, onMouseUp: () => { this.mouseDown = false; }, onClick: getCallback(this.handleClick), onDoubleClick: getCallback(this.handleDoubleClick), onKeyDown: (event) => { if (event.key === ' ') { // display item actions when space is clicked const el = event.target?.querySelector('.more-activity-toggle-ref'); if (typeof el?.click === 'function') { event.preventDefault(); el.click(); } } }, draggable: !this.props.isNested, tabIndex: 0, 'data-test-id': 'article-item', }, (
{getTemplate()}
), getNested(), ); } }