From 47a308c04a99fe71ba8acba08e015bb03b26d79b Mon Sep 17 00:00:00 2001 From: Konstantin Markov Date: Fri, 11 Oct 2024 15:06:18 +0300 Subject: [PATCH] Add to planning refactoring (#1907) --- client/actions/assignments/api.ts | 4 +- .../AssignmentPreviewContainer/index.tsx | 4 +- .../Planning/PlanningEditor/index.tsx | 7 +- .../Planning/PlanningPreviewContent.tsx | 3 +- .../controllers/AddToPlanningController.tsx | 33 +++- client/extension_bridge.ts | 4 +- client/planning-extension/src/extension.ts | 151 +++++++++++------- .../src/extension_bridge.ts | 4 +- index.ts | 65 +++----- 9 files changed, 161 insertions(+), 114 deletions(-) diff --git a/client/actions/assignments/api.ts b/client/actions/assignments/api.ts index bec65419c..8d00b041d 100644 --- a/client/actions/assignments/api.ts +++ b/client/actions/assignments/api.ts @@ -10,7 +10,7 @@ import * as actions from '../'; import {ASSIGNMENTS, ALL_DESKS, SORT_DIRECTION} from '../../constants'; import planningUtils from '../../utils/planning'; import {getErrorMessage, isExistingItem, gettext} from '../../utils'; -import planning from '../planning'; +import planningActions from '../planning/api'; import {assignmentsViewRequiresArchiveItems} from '../../components/Assignments/AssignmentItem/fields'; const setBaseQuery = ({must = []}) => ({ @@ -415,7 +415,7 @@ const receiveAssignmentHistory = (items) => ({ * @param {object} assignment - The Assignment to load items for */ const loadPlanningAndEvent = (assignment) => (dispatch) => - dispatch(planning.api.fetchById(assignment.planning_item)); + dispatch(planningActions.fetchById(assignment.planning_item)); /** * Loads the Archive items that are linked to the provided Assignment list diff --git a/client/components/Assignments/AssignmentPreviewContainer/index.tsx b/client/components/Assignments/AssignmentPreviewContainer/index.tsx index 16d944a5e..791d81ab4 100644 --- a/client/components/Assignments/AssignmentPreviewContainer/index.tsx +++ b/client/components/Assignments/AssignmentPreviewContainer/index.tsx @@ -16,6 +16,8 @@ import { import {superdeskApi} from '../../../superdeskApi'; import * as selectors from '../../../selectors'; import * as actions from '../../../actions'; +import planningActions from '../../../actions/planning/api'; + import {assignmentUtils, eventUtils, planningUtils, getFileDownloadURL} from '../../../utils'; import {ASSIGNMENTS, WORKSPACE} from '../../../constants'; @@ -262,7 +264,7 @@ const mapDispatchToProps = (dispatch) => ({ removeAssignment: (assignment) => dispatch(actions.assignments.ui.showRemoveAssignmentModal(assignment)), openArchivePreview: (assignment) => dispatch(actions.assignments.ui.openArchivePreview(assignment)), fetchEventFiles: (event) => dispatch(actions.events.api.fetchEventFiles(event)), - fetchPlanningFiles: (planning) => dispatch(actions.planning.api.fetchPlanningFiles(planning)), + fetchPlanningFiles: (planning) => dispatch(planningActions.fetchPlanningFiles(planning)), }); export const AssignmentPreviewContainer = connect( diff --git a/client/components/Planning/PlanningEditor/index.tsx b/client/components/Planning/PlanningEditor/index.tsx index 261a0927d..cfc10d996 100644 --- a/client/components/Planning/PlanningEditor/index.tsx +++ b/client/components/Planning/PlanningEditor/index.tsx @@ -27,6 +27,7 @@ import {planningUtils, eventUtils, lockUtils} from '../../../utils'; import {EditorForm} from '../../Editor/EditorForm'; import {PlanningEditorHeader} from './PlanningEditorHeader'; import {COVERAGES} from '../../../constants'; +import planningActions from '../../../actions/planning/api'; interface IProps { original?: IPlanningItem; @@ -97,9 +98,9 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => ({ fetchEventFiles: (event) => dispatch(actions.events.api.fetchEventFiles(event)), - fetchPlanningFiles: (planning) => dispatch(actions.planning.api.fetchPlanningFiles(planning)), - uploadFiles: (files) => dispatch(actions.planning.api.uploadFiles({files: files})), - removeFile: (file) => dispatch(actions.planning.api.removeFile(file)), + fetchPlanningFiles: (planning) => dispatch(planningActions.fetchPlanningFiles(planning)), + uploadFiles: (files) => dispatch(planningActions.uploadFiles({files: files})), + removeFile: (file) => dispatch(planningActions.removeFile(file)), setCoverageDefaultDesk: (coverage) => dispatch(actions.users.setCoverageDefaultDesk(coverage)), setCoverageAddAdvancedMode: (advancedMode) => dispatch(actions.users.setCoverageAddAdvancedMode(advancedMode)), }); diff --git a/client/components/Planning/PlanningPreviewContent.tsx b/client/components/Planning/PlanningPreviewContent.tsx index 5ba7ee0f4..6595b7a41 100644 --- a/client/components/Planning/PlanningPreviewContent.tsx +++ b/client/components/Planning/PlanningPreviewContent.tsx @@ -19,6 +19,7 @@ import {eventUtils, getCreator, getFileDownloadURL} from '../../utils'; import {getUserInterfaceLanguageFromCV} from '../../utils/users'; import * as selectors from '../../selectors'; import * as actions from '../../actions'; +import planningActions from '../../actions/planning/api'; import { AuditInformation, @@ -71,7 +72,7 @@ const mapStateToProps = (state, ownProps) => ({ const mapDispatchToProps = (dispatch) => ({ onEditEvent: (event) => dispatch(actions.main.openForEdit(event)), fetchEventFiles: (event) => dispatch(actions.events.api.fetchEventFiles(event)), - fetchPlanningFiles: (planning) => dispatch(actions.planning.api.fetchPlanningFiles(planning)), + fetchPlanningFiles: (planning) => dispatch(planningActions.fetchPlanningFiles(planning)), }); export class PlanningPreviewContentComponent extends React.PureComponent { diff --git a/client/controllers/AddToPlanningController.tsx b/client/controllers/AddToPlanningController.tsx index 90675f800..0ff631dd3 100644 --- a/client/controllers/AddToPlanningController.tsx +++ b/client/controllers/AddToPlanningController.tsx @@ -5,11 +5,12 @@ import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import {ModalsContainer} from '../components'; import {planning} from '../actions'; -import {get, isEmpty, isNumber} from 'lodash'; +import {get, isEmpty, isNumber, noop} from 'lodash'; import {registerNotifications, getErrorMessage, isExistingItem} from '../utils'; import {WORKSPACE, MODALS} from '../constants'; import {GET_LABEL_MAP} from 'superdesk-core/scripts/apps/workspace/content/constants'; import {IArticle, IContentProfile} from 'superdesk-api'; +import {authoringReactViewEnabled} from 'appConfig'; import {planningApi, superdeskApi} from '../superdeskApi'; import {PLANNING_VIEW} from '../interfaces'; @@ -87,7 +88,7 @@ export class AddToPlanningController { return sdPlanningStore.initWorkspace(WORKSPACE.AUTHORING, this.loadWorkspace) .then( this.render, - this.$scope.resolve + !authoringReactViewEnabled ? this.$scope.resolve : noop ); } @@ -190,7 +191,9 @@ export class AddToPlanningController { this.store.dispatch(actions.resetStore()); if (this.superdeskFlags.flags.authoring || !this.rendered) { - this.$scope.resolve(); + if (!authoringReactViewEnabled) { + this.$scope.resolve(); + } return; } @@ -205,7 +208,9 @@ export class AddToPlanningController { body: this.gettext('The item was unlocked by "{{ username }}"', {username}), action: () => { this.newsItem.lock_session = null; - this.$scope.resolve(); + if (!authoringReactViewEnabled) { + this.$scope.resolve(); + } }, }, }))); @@ -263,7 +268,9 @@ export class AddToPlanningController { this.notify.error(err); }); - this.$scope.resolve('foo'); + if (!authoringReactViewEnabled) { + this.$scope.resolve('foo'); + } return Promise.reject('foo'); } @@ -271,7 +278,9 @@ export class AddToPlanningController { this.notify.error( this.gettext('Item already locked.') ); - this.$scope.resolve('bar'); + if (!authoringReactViewEnabled) { + this.$scope.resolve('bar'); + } return Promise.reject('bar'); } @@ -284,13 +293,23 @@ export class AddToPlanningController { this.notify.error( getErrorMessage(error, this.gettext('Failed to lock the item.')) ); - this.$scope.resolve(error); + if (!authoringReactViewEnabled) { + this.$scope.resolve(error); + } return Promise.reject(error); } ); } return Promise.resolve(newsItem); + }, (error) => { + this.notify.error( + getErrorMessage(error, this.gettext('Failed to load the item.')) + ); + if (!authoringReactViewEnabled) { + this.$scope.resolve(error); + } + return Promise.reject(error); }); } } diff --git a/client/extension_bridge.ts b/client/extension_bridge.ts index 5f70d5b1c..4f26a9c3f 100644 --- a/client/extension_bridge.ts +++ b/client/extension_bridge.ts @@ -10,10 +10,10 @@ import {EditorFieldVocabulary, IEditorFieldVocabularyProps} from './components/f import {getVocabularyItemFieldTranslated} from './utils/vocabularies'; import {getUserInterfaceLanguageFromCV} from './utils/users'; -import {isContentLinkToCoverageAllowed} from './utils/archive'; import {registerEditorField} from './components/fields/resources/registerEditorFields'; import {IAssignmentItem, IEditorFieldProps, IPlanningAppState, IPlanningItem} from 'interfaces'; +import {isContentLinkToCoverageAllowed} from './utils/archive'; import PlanningDetailsWidget, {getItemPlanningInfo} from './components/PlanningDetailsWidget'; @@ -48,6 +48,7 @@ interface IExtensionBridge { language?: string, fallbackField?: string ): string; + isContentLinkToCoverageAllowed(item: IArticle): boolean; }; components: { @@ -91,6 +92,7 @@ export const extensionBridge: IExtensionBridge = { utils: { getUserInterfaceLanguageFromCV, getVocabularyItemFieldTranslated, + isContentLinkToCoverageAllowed, }, components: { EditorFieldVocabulary, diff --git a/client/planning-extension/src/extension.ts b/client/planning-extension/src/extension.ts index d66035e79..f4c03102f 100644 --- a/client/planning-extension/src/extension.ts +++ b/client/planning-extension/src/extension.ts @@ -5,6 +5,7 @@ import { ISuperdesk, IExtensionActivationResult, onPublishMiddlewareResult, + IAuthoringAction, } from 'superdesk-api'; import {IPlanningAssignmentService} from './interfaces'; import {IPlanningConfig} from '../../interfaces'; @@ -113,69 +114,104 @@ const extension: IExtension = { const displayTopbarWidget = superdesk.privileges.hasPrivilege('planning_assignments_view') && extensionConfig?.assignmentsTopBarWidget === true; const {gettext} = superdesk.localization; + const {article: superdeskArticleApi} = superdesk.entities; const planningActionsGroupId = 'planning-actions'; const {getItemPlanningInfo} = extensionBridge.planning; + const canAddToPlanning = (item: IArticle) => ( + superdesk.privileges.hasPrivilege('planning_planning_management') && + superdesk.privileges.hasPrivilege('archive') && + !item.assignment_id != null && + !superdesk.entities.article.isPersonal(item) && + !superdesk.entities.article.isLockedInOtherSession(item) && + item.state !== 'correction' && + extensionBridge.ui.utils.isContentLinkToCoverageAllowed(item) && + ( + superdesk.entities.article.itemAction(item).edit || + superdesk.entities.article.itemAction(item).correct || + superdesk.entities.article.itemAction(item).deschedule + ) + ); + const canUnlinkAsCoverage = (item: IArticle) => ( + superdesk.privileges.hasPrivilege('archive') && + item.assignment_id != null && + !superdeskArticleApi.isPersonal(item) && + !superdeskArticleApi.isLockedInOtherSession(item) && + ( + superdeskArticleApi.itemAction(item).edit || + superdeskArticleApi.itemAction(item).correct || + superdeskArticleApi.itemAction(item).deschedule + ) + ); + const canFulfilAssignment = (item: IArticle) => { + const itemStates = ['killed', 'recalled', 'unpublished', 'spiked', 'correction']; + const {isContentLinkToCoverageAllowed} = extensionBridge.assignments.utils; + + return ( + !item.assignment_id && + !superdesk.entities.article.isPersonal(item) && + isContentLinkToCoverageAllowed(item) && + !superdesk.entities.article.isLockedInOtherSession(item) && + !itemStates.includes(item.state) && + superdesk.privileges.hasPrivilege('archive') + ); + }; + + const getPermittedActions = (item: IArticle) => { + const permittedActions: Array = []; + + if (canAddToPlanning(item)) { + permittedActions.push({ + label: gettext('Add to Planning'), + groupId: planningActionsGroupId, + icon: 'calendar-list', + onTrigger: () => { + const customEvent = new CustomEvent('planning:addToPlanning', {detail: item}); + + window.dispatchEvent(customEvent); + }, + }); + } + + if (canUnlinkAsCoverage(item)) { + permittedActions.push({ + label: gettext('Unlink as Coverage'), + groupId: planningActionsGroupId, + icon: 'cut', + onTrigger: () => { + superdesk.entities.article.get(item._id).then((_item) => { + window.dispatchEvent(new CustomEvent( + 'planning:unlinkfromcoverage', + {detail: {item: _item}}, + )); + }); + }, + }); + } + + if (canFulfilAssignment(item)) { + permittedActions.push({ + label: superdesk.localization.gettext('Fulfil assignment'), + groupId: planningActionsGroupId, + icon: 'calendar-list', + onTrigger: () => { + superdesk.entities.article.get(item._id).then((_item) => { + window.dispatchEvent(new CustomEvent( + 'planning:fulfilassignment', + {detail: {item: _item}}, + )); + }); + }, + }); + } + + return permittedActions; + }; const result: IExtensionActivationResult = { contributions: { entities: { article: { - getActions: (item) => [ - { - label: gettext('Unlink as Coverage'), - groupId: planningActionsGroupId, - icon: 'cut', - onTrigger: () => { - const superdeskArticle = superdesk.entities.article; - - if ( - superdesk.privileges.hasPrivilege('archive') && - item.assignment_id != null && - !superdeskArticle.isPersonal(item) && - !superdeskArticle.isLockedInOtherSession(item) && - ( - superdeskArticle.itemAction(item).edit || - superdeskArticle.itemAction(item).correct || - superdeskArticle.itemAction(item).deschedule - ) - ) { - superdeskArticle.get(item._id).then((_item) => { - window.dispatchEvent(new CustomEvent( - 'planning:unlinkfromcoverage', - {detail: {item: _item}}, - )); - }); - } - }, - }, - { - label: superdesk.localization.gettext('Fulfil assignment'), - groupId: planningActionsGroupId, - icon: 'calendar-list', - onTrigger: () => { - const itemStates = ['killed', 'recalled', 'unpublished', 'spiked', 'correction']; - const {isContentLinkToCoverageAllowed} = extensionBridge.assignments.utils; - - if ( - !item.assignment_id && - !superdesk.entities.article.isPersonal(item) && - isContentLinkToCoverageAllowed(item) && - !superdesk.entities.article.isLockedInOtherSession(item) && - !itemStates.includes(item.state) && - superdesk.privileges.hasPrivilege('archive') - ) { - superdesk.entities.article.get(item._id).then((_item) => { - window.dispatchEvent(new CustomEvent( - 'planning:fulfilassignment', - {detail: {item: _item}}, - )); - }); - } else { - superdesk.ui.notify.error('This action is not permitted'); - } - }, - } - ], + getActions: (item) => getPermittedActions(item), onSpike: (item: IArticle) => onSpike(superdesk, item), onSpikeMultiple: (items: Array) => onSpikeMultiple(superdesk, items), onPublish: (item: IArticle) => onPublishArticle(superdesk, item), @@ -191,6 +227,9 @@ const extension: IExtension = { }, }, }, + + // Current set of planning actions have to be displayed both in authoring and list item actions + getAuthoringActions: (item) => getPermittedActions(item), notifications: { 'email:notification:assignments': {name: superdesk.localization.gettext('Assignment')} }, diff --git a/client/planning-extension/src/extension_bridge.ts b/client/planning-extension/src/extension_bridge.ts index e9cceace7..d2aa9ff00 100644 --- a/client/planning-extension/src/extension_bridge.ts +++ b/client/planning-extension/src/extension_bridge.ts @@ -1,7 +1,6 @@ import * as React from 'react'; -import {IVocabularyItem} from 'superdesk-api'; +import {IArticle, IVocabularyItem} from 'superdesk-api'; import {IAssignmentItem, IEditorFieldProps, IPlanningAppState, IPlanningItem} from '../../interfaces'; -import {IArticle} from 'superdesk-api'; interface IEditorFieldVocabularyProps extends IEditorFieldProps { options: Array; @@ -44,6 +43,7 @@ interface IExtensionBridge { language?: string, fallbackField?: string ): string; + isContentLinkToCoverageAllowed(item: IArticle): boolean; }; components: { EditorFieldVocabulary: React.ComponentType; diff --git a/index.ts b/index.ts index d57d65f53..33cc17ea5 100644 --- a/index.ts +++ b/index.ts @@ -1,14 +1,8 @@ // styles import './client/styles/index.scss'; - -import {IArticle} from 'superdesk-api'; -import {superdeskApi} from './client/superdeskApi'; - -// scripts import planningModule from './client'; import * as ctrl from './client/controllers'; import {gettext} from './client/utils/gettext'; -import {isContentLinkToCoverageAllowed} from './client/utils/archive'; import ng from 'superdesk-core/scripts/core/services/ng'; configurePlanning.$inject = ['superdeskProvider']; @@ -40,41 +34,6 @@ function configurePlanning(superdesk) { privileges: { planning_locations_management: 1, }, - }) - .activity('planning.addto', { - label: gettext('Add to Planning'), - modal: true, - icon: 'calendar-list', - priority: 3000, - controller: ctrl.AddToPlanningController, - filters: [ - { - action: 'list', - type: 'archive', - }, - { - action: 'external-app', - type: 'addto-planning', - }, - ], - group: gettext('Planning'), - privileges: { - planning_planning_management: 1, - archive: 1, - }, - additionalCondition: ['archiveService', 'item', 'authoring', - function(archiveService, item: IArticle, authoring) { - return !item.assignment_id && - !archiveService.isPersonal(item) && - !superdeskApi.entities.article.isLockedInOtherSession(item) && - !['correction'].includes(item.state) && - isContentLinkToCoverageAllowed(item) && - ( - authoring.itemActions(item).edit || - authoring.itemActions(item).correct || - authoring.itemActions(item).deschedule - ); - }], }); } @@ -106,6 +65,30 @@ window.addEventListener('planning:fulfilassignment', (event: CustomEvent) => { ); }); +window.addEventListener('planning:addToPlanning', (e: CustomEvent) => { + const newElement = document.createElement('div'); + const jQueryElement = window.$(newElement); + const rootScope = ng.get('$rootScope'); + + newElement.className = 'modal__dialog ng-scope'; + rootScope.locals = {data: {item: e.detail}}; + + rootScope.resolve = () => newElement.remove(); + + new ctrl.AddToPlanningController( + jQueryElement, + rootScope, + ng.get('sdPlanningStore'), + ng.get('notify'), + ng.get('gettext'), + ng.get('api'), + ng.get('lock'), + ng.get('session'), + ng.get('userList'), + ng.get('$timeout'), + ng.get('superdeskFlags'), + ); +}); window.addEventListener('planning:unlinkfromcoverage', (event: CustomEvent) => { ctrl.UnlinkAssignmentController(