From af644e40c2d3590c4e76166e8cbb4049c5327582 Mon Sep 17 00:00:00 2001 From: Nikola Stojanovic <68916411+dzonidoo@users.noreply.github.com> Date: Tue, 7 Feb 2023 12:58:26 +0100 Subject: [PATCH] Authoring-react keybindings (#4196) --- .../authoring-angular-integration.tsx | 21 +- .../authoring-integration-wrapper.tsx | 83 ++-- .../apps/authoring-react/authoring-react.tsx | 373 +++++++++++------- .../authoring-react/fields/date/editor.tsx | 2 +- .../authoring-react/fields/register-fields.ts | 9 +- .../apps/authoring-react/multi-edit-modal.tsx | 1 + .../subcomponents/authoring-actions-menu.tsx | 6 +- .../subcomponents/lock-info.tsx | 3 +- .../mark-for-desks/mark-for-desks-modal.tsx | 2 +- .../apps/authoring-react/with-keybindings.tsx | 45 +++ .../directives/AuthoringTopbarDirective.ts | 6 +- scripts/apps/highlights/views/menu.html | 2 +- .../monitoring/directives/ItemActionsMenu.ts | 172 ++++---- .../components/actions-menu/MenuItems.tsx | 6 +- scripts/core/superdesk-api-helpers.tsx | 9 +- scripts/core/superdesk-api.d.ts | 17 +- 16 files changed, 469 insertions(+), 288 deletions(-) create mode 100644 scripts/apps/authoring-react/with-keybindings.tsx diff --git a/scripts/apps/authoring-react/authoring-angular-integration.tsx b/scripts/apps/authoring-react/authoring-angular-integration.tsx index a83b1071a1..570f3a3352 100644 --- a/scripts/apps/authoring-react/authoring-angular-integration.tsx +++ b/scripts/apps/authoring-react/authoring-angular-integration.tsx @@ -57,6 +57,13 @@ function getInlineToolbarActions(options: IExposedFromAuthoring): IAut /> ), availableOffline: true, + keyBindings: { + 'ctrl+shift+s': () => { + if (hasUnsavedChanges()) { + save(); + } + }, + }, }; const closeButton: ITopBarWidget = { @@ -72,6 +79,11 @@ function getInlineToolbarActions(options: IExposedFromAuthoring): IAut /> ), availableOffline: true, + keyBindings: { + 'ctrl+shift+e': () => { + initiateClosing(); + }, + }, }; const minimizeButton: ITopBarWidget = { @@ -146,7 +158,6 @@ function getInlineToolbarActions(options: IExposedFromAuthoring): IAut actions.push(getManageHighlights()); } - // eslint-disable-next-line no-case-declarations const manageDesksButton: ITopBarWidget = ({ group: 'start', @@ -196,8 +207,16 @@ function getInlineToolbarActions(options: IExposedFromAuthoring): IAut unlock={() => { stealLock(); }} + isLockedInOtherSession={(article) => sdApi.article.isLockedInOtherSession(article)} /> ), + keyBindings: { + 'ctrl+shift+u': () => { + if (sdApi.article.isLockedInOtherSession(item)) { + stealLock(); + } + }, + }, availableOffline: false, }); diff --git a/scripts/apps/authoring-react/authoring-integration-wrapper.tsx b/scripts/apps/authoring-react/authoring-integration-wrapper.tsx index 2b4a8d1f8c..c879dfa75a 100644 --- a/scripts/apps/authoring-react/authoring-integration-wrapper.tsx +++ b/scripts/apps/authoring-react/authoring-integration-wrapper.tsx @@ -47,14 +47,13 @@ import {CompareArticleVersionsModal} from './toolbar/compare-article-versions'; import {httpRequestJsonLocal} from 'core/helpers/network'; import {getArticleAdapter} from './article-adapter'; import {ui} from 'core/ui-utils'; -import TranslateModal from './toolbar/translate-modal'; import {MarkForDesksModal} from './toolbar/mark-for-desks/mark-for-desks-modal'; function getAuthoringActionsFromExtensions( item: IArticle, contentProfile: IContentProfileV2, fieldsData: Map, -): Promise> { +): Array { const actionGetters : Array = flatMap( @@ -62,10 +61,9 @@ function getAuthoringActionsFromExtensions( (extension) => extension.activationResult.contributions?.getAuthoringActions ?? [], ); - return Promise.all(actionGetters.map((getPromise) => getPromise(item, contentProfile, fieldsData))) - .then((res) => { - return flatMap(res); - }); + return flatMap( + actionGetters.map((getPromise) => getPromise(item, contentProfile, fieldsData)), + ); } const defaultToolbarItems: Array> = [CreatedModifiedInfo]; @@ -183,18 +181,27 @@ const getExportModal = ( }); const getHighlightsAction = (getItem: () => IArticle): IAuthoringAction => { + const showHighlightsModal = () => { + showModal(({closeModal}) => { + return ( + + ); + }); + }; + return { label: gettext('Highlights'), onTrigger: () => ( - showModal(({closeModal}) => { - return ( - - ); - }) + showHighlightsModal() ), + keyBindings: { + 'ctrl+shift+h': () => { + showHighlightsModal(); + }, + }, }; }; @@ -212,7 +219,6 @@ const getSaveAsTemplate = (getItem: () => IArticle): IAuthoringAction => ({ ), }); - const getTranslateModal = (getItem: () => IArticle): IAuthoringAction => ({ label: gettext('Translate'), onTrigger: () => { @@ -375,28 +381,28 @@ export class AuthoringIntegrationWrapper extends React.PureComponent { - return Promise.all([ - getAuthoringActionsFromExtensions(item, contentProfile, fieldsData), - getArticleActionsFromExtensions(item), - ]).then((res) => { - const [authoringActionsFromExtensions, articleActionsFromExtensions] = res; - - return [ - getSaveAsTemplate(getLatestItem), - getCompareVersionsModal( - getLatestItem, - authoringStorage, - fieldsAdapter, - storageAdapter, - ), - getHighlightsAction(getLatestItem), - getMarkedForDesksModal(getLatestItem), - getExportModal(getLatestItem, handleUnsavedChanges, hasUnsavedChanges), - getTranslateModal(getLatestItem), - ...authoringActionsFromExtensions, - ...articleActionsFromExtensions, - ]; - }); + const authoringActionsFromExtensions = getAuthoringActionsFromExtensions( + item, + contentProfile, + fieldsData, + ); + const articleActionsFromExtensions = getArticleActionsFromExtensions(item); + + return [ + getSaveAsTemplate(getLatestItem), + getCompareVersionsModal( + getLatestItem, + authoringStorage, + fieldsAdapter, + storageAdapter, + ), + getHighlightsAction(getLatestItem), + getMarkedForDesksModal(getLatestItem), + getExportModal(getLatestItem, handleUnsavedChanges, hasUnsavedChanges), + getTranslateModal(getLatestItem), + ...authoringActionsFromExtensions, + ...articleActionsFromExtensions, + ]; }} getInlineToolbarActions={this.props.getInlineToolbarActions} getAuthoringTopBarWidgets={ @@ -473,6 +479,9 @@ export class AuthoringIntegrationWrapper extends React.PureComponent { + return getWidgetsFromExtensions(article)[index].label; + }} /> ); }} diff --git a/scripts/apps/authoring-react/authoring-react.tsx b/scripts/apps/authoring-react/authoring-react.tsx index def32667c3..c8239b0c63 100644 --- a/scripts/apps/authoring-react/authoring-react.tsx +++ b/scripts/apps/authoring-react/authoring-react.tsx @@ -12,6 +12,7 @@ import { IPropsAuthoring, ITopBarWidget, IExposedFromAuthoring, + IKeyBindings, } from 'superdesk-api'; import { ButtonGroup, @@ -45,6 +46,7 @@ import {getField} from 'apps/fields'; import {preferences} from 'api/preferences'; import {dispatchEditorEvent, addEditorEventListener} from './authoring-react-editor-events'; import {previewAuthoringEntity} from './preview-article-modal'; +import {WithKeyBindings} from './with-keybindings'; export function getFieldsData( item: T, @@ -190,6 +192,17 @@ function getInitialState( return initialState; } +function getKeyBindingsFromActions(actions: Array>): IKeyBindings { + return actions + .filter((action) => action.keyBindings != null) + .reduce((acc, action) => { + return { + ...acc, + ...action.keyBindings, + }; + }, {}); +} + /** * Toggling a field "off" hides it and removes its values. * Toggling to "on", displays field's input and allows setting a value. @@ -1043,6 +1056,118 @@ export class AuthoringReact extends React.PureCo const readOnly = state.initialized ? authoringOptions.readOnly : false; const OpenWidgetComponent = getSidePanel == null ? null : this.props.getSidePanel(exposed, readOnly); + const authoringActions: Array = (() => { + const actions = this.props.getActions?.(exposed) ?? []; + const coreActions: Array = []; + + if (appConfig.features.useTansaProofing !== true) { + if (state.spellcheckerEnabled) { + const nextValue = false; + + coreActions.push({ + label: gettext('Disable spellchecker'), + onTrigger: () => { + this.setState({ + ...state, + spellcheckerEnabled: nextValue, + }); + + dispatchEditorEvent('spellchecker__set_status', nextValue); + + preferences.update(SPELLCHECKER_PREFERENCE, { + type: 'bool', + enabled: nextValue, + default: true, + }); + }, + keyBindings: { + 'ctrl+shift+y': () => { + this.setState({ + ...state, + spellcheckerEnabled: nextValue, + }); + + dispatchEditorEvent('spellchecker__set_status', nextValue); + + preferences.update(SPELLCHECKER_PREFERENCE, { + type: 'bool', + enabled: nextValue, + default: true, + }); + }, + }, + }); + } else { + coreActions.push({ + label: gettext('Enable spellchecker'), + onTrigger: () => { + const nextValue = true; + + this.setState({ + ...state, + spellcheckerEnabled: true, + }); + + dispatchEditorEvent('spellchecker__set_status', nextValue); + + preferences.update(SPELLCHECKER_PREFERENCE, { + type: 'bool', + enabled: nextValue, + default: true, + }); + }, + keyBindings: { + 'ctrl+shift+y': () => { + const nextValue = true; + + this.setState({ + ...state, + spellcheckerEnabled: true, + }); + + dispatchEditorEvent('spellchecker__set_status', nextValue); + + preferences.update(SPELLCHECKER_PREFERENCE, { + type: 'bool', + enabled: nextValue, + default: true, + }); + }, + }, + }); + } + } + + return [...coreActions, ...actions]; + })(); + + const keyBindingsFromAuthoringActions: IKeyBindings = authoringActions.reduce((acc, action) => { + return { + ...acc, + ...(action.keyBindings ?? {}), + }; + }, {}); + + const widgetsCount = this.props.getSidebar(exposed).props.items.length; + + const widgetKeybindings: IKeyBindings = {}; + + for (let i = 0; i < widgetsCount; i++) { + widgetKeybindings[`ctrl+alt+${i + 1}`] = () => { + const nextWidgetName: string = this.props.getSideWidgetNameAtIndex(exposed.item, i); + + const nextState: IStateLoaded = { + ...state, + openWidget: { + name: nextWidgetName, + pinned: state.openWidget?.pinned ?? false, + }, + }; + + this.setState(nextState); + }; + } + const toolbar1Widgets: Array> = [ ...authoringOptions.actions, { @@ -1050,61 +1175,7 @@ export class AuthoringReact extends React.PureCo priority: 0.4, component: () => { return ( - { - return ( - this.props.getActions?.(exposed) ?? Promise.resolve([]) - ).then((actions) => { - const coreActions: Array = []; - - if (appConfig.features.useTansaProofing !== true) { - if (state.spellcheckerEnabled) { - const nextValue = false; - - coreActions.push({ - label: gettext('Disable spellchecker'), - onTrigger: () => { - this.setState({ - ...state, - spellcheckerEnabled: nextValue, - }); - - dispatchEditorEvent('spellchecker__set_status', nextValue); - - preferences.update(SPELLCHECKER_PREFERENCE, { - type: 'bool', - enabled: nextValue, - default: true, - }); - }, - }); - } else { - coreActions.push({ - label: gettext('Enable spellchecker'), - onTrigger: () => { - const nextValue = true; - - this.setState({ - ...state, - spellcheckerEnabled: true, - }); - - dispatchEditorEvent('spellchecker__set_status', nextValue); - - preferences.update(SPELLCHECKER_PREFERENCE, { - type: 'bool', - enabled: nextValue, - default: true, - }); - }, - }); - } - } - - return [...coreActions, ...actions]; - }); - }} - /> + authoringActions} /> ); }, availableOffline: true, @@ -1113,6 +1184,43 @@ export class AuthoringReact extends React.PureCo const pinned = state.openWidget?.pinned === true; + const printPreviewAction = (() => { + const execute = () => { + previewAuthoringEntity( + state.profile, + state.fieldsDataWithChanges, + ); + }; + + const preview = { + jsxButton: () => { + return ( + { + execute(); + }} + /> + ); + }, + keybindings: { + 'ctrl+shift+i': () => { + execute(); + }, + }, + }; + + return preview; + })(); + + const allKeyBindings: IKeyBindings = { + ...printPreviewAction.keybindings, + ...getKeyBindingsFromActions(authoringOptions.actions), + ...keyBindingsFromAuthoringActions, + ...widgetKeybindings, + }; + return ( { @@ -1121,67 +1229,76 @@ export class AuthoringReact extends React.PureCo ) } - - {(panelState, panelActions) => { - return ( - - - - )} - main={( - -
- { - this.props.topBar2Widgets - .map((Component, i) => { - return ( - - ); - }) - } -
- - - { - previewAuthoringEntity( - state.profile, - state.fieldsDataWithChanges, - ); + + + {(panelState, panelActions) => { + return ( + + + + )} + main={( + +
+ { + this.props.topBar2Widgets + .map((Component, i) => { + return ( + + ); + }) + } +
+ + + {printPreviewAction.jsxButton()} + +
+ )} + authoringHeader={( +
+ - - - )} - authoringHeader={( +
+ )} + >
extends React.PureCo validationErrors={state.validationErrors} />
- )} - > -
- -
- - )} - sideOverlay={!pinned && OpenWidgetComponent != null && OpenWidgetComponent} - sideOverlayOpen={!pinned && OpenWidgetComponent != null} - sidePanel={pinned && OpenWidgetComponent != null && OpenWidgetComponent} - sidePanelOpen={pinned && OpenWidgetComponent != null} - sideBar={this.props.getSidebar?.(exposed)} - /> - ); - }} - + + )} + sideOverlay={!pinned && OpenWidgetComponent != null && OpenWidgetComponent} + sideOverlayOpen={!pinned && OpenWidgetComponent != null} + sidePanel={pinned && OpenWidgetComponent != null && OpenWidgetComponent} + sidePanelOpen={pinned && OpenWidgetComponent != null} + sideBar={this.props.getSidebar?.(exposed)} + /> + ); + }} + + ); } diff --git a/scripts/apps/authoring-react/fields/date/editor.tsx b/scripts/apps/authoring-react/fields/date/editor.tsx index d2a441a15a..8929a86703 100644 --- a/scripts/apps/authoring-react/fields/date/editor.tsx +++ b/scripts/apps/authoring-react/fields/date/editor.tsx @@ -30,7 +30,7 @@ export class Editor extends React.PureComponent { }} dateFormat={appConfig.view.dateformat} locale={getLocaleForDatePicker(this.props.language)} - shortcuts={this.props.config?.shortcuts?.map(({label, value, term}) => { + headerButtonBar={this.props.config?.shortcuts?.map(({label, value, term}) => { return { label, days: differenceInCalendarDays( diff --git a/scripts/apps/authoring-react/fields/register-fields.ts b/scripts/apps/authoring-react/fields/register-fields.ts index 48170470be..fd4ed3af57 100644 --- a/scripts/apps/authoring-react/fields/register-fields.ts +++ b/scripts/apps/authoring-react/fields/register-fields.ts @@ -25,11 +25,16 @@ export function registerAuthoringReactFields() { onTrigger: () => { runTansa(contentProfile, fieldsData); }, + keyBindings: { + 'ctrl+shift+y': () => { + runTansa(contentProfile, fieldsData); + }, + }, }; - return Promise.resolve([checkSpellingAction]); + return [checkSpellingAction]; } else { - return Promise.resolve([]); + return []; } }, customFieldTypes: [ diff --git a/scripts/apps/authoring-react/multi-edit-modal.tsx b/scripts/apps/authoring-react/multi-edit-modal.tsx index 6b57c944cd..5e8aaadd26 100644 --- a/scripts/apps/authoring-react/multi-edit-modal.tsx +++ b/scripts/apps/authoring-react/multi-edit-modal.tsx @@ -141,6 +141,7 @@ export class MultiEditModal extends React.PureComponent { unlock={() => { stealLock(); }} + isLockedInOtherSession={(article) => sdApi.article.isLockedInOtherSession(article)} /> ), availableOffline: false, diff --git a/scripts/apps/authoring-react/subcomponents/authoring-actions-menu.tsx b/scripts/apps/authoring-react/subcomponents/authoring-actions-menu.tsx index 75f3bcccee..2a28983be6 100644 --- a/scripts/apps/authoring-react/subcomponents/authoring-actions-menu.tsx +++ b/scripts/apps/authoring-react/subcomponents/authoring-actions-menu.tsx @@ -6,7 +6,7 @@ import {gettext} from 'core/utils'; import {IMenuItem} from 'superdesk-ui-framework/react/components/Menu'; interface IProps { - getActions: () => Promise>; + getActions: () => Array; } interface IState { @@ -25,9 +25,7 @@ export class AuthoringActionsMenu extends React.PureComponent { } getActions() { - this.props.getActions().then((actions) => { - this.setState({actions}); - }); + this.setState({actions: this.props.getActions()}); } render() { diff --git a/scripts/apps/authoring-react/subcomponents/lock-info.tsx b/scripts/apps/authoring-react/subcomponents/lock-info.tsx index 4589c2eebc..e8d3282e09 100644 --- a/scripts/apps/authoring-react/subcomponents/lock-info.tsx +++ b/scripts/apps/authoring-react/subcomponents/lock-info.tsx @@ -10,6 +10,7 @@ import {store} from 'core/data'; interface IProps { article: IArticle; unlock(): void; + isLockedInOtherSession(article: IArticle): boolean; } interface IState { @@ -63,7 +64,7 @@ class LockInfoComponent extends React.PureComponent { export class LockInfo extends React.PureComponent { render() { - if (sdApi.article.isLockedInOtherSession(this.props.article) !== true) { + if (this.props.isLockedInOtherSession(this.props.article) !== true) { return null; } diff --git a/scripts/apps/authoring-react/toolbar/mark-for-desks/mark-for-desks-modal.tsx b/scripts/apps/authoring-react/toolbar/mark-for-desks/mark-for-desks-modal.tsx index 7cdd3f7c18..7250908064 100644 --- a/scripts/apps/authoring-react/toolbar/mark-for-desks/mark-for-desks-modal.tsx +++ b/scripts/apps/authoring-react/toolbar/mark-for-desks/mark-for-desks-modal.tsx @@ -39,7 +39,7 @@ export class MarkForDesksModal extends React.PureComponent { > { + onChange={(value) => { this.setState({ ...this.state, selectedDesks: value.map((desk) => desk._id), diff --git a/scripts/apps/authoring-react/with-keybindings.tsx b/scripts/apps/authoring-react/with-keybindings.tsx new file mode 100644 index 0000000000..97793fc5ba --- /dev/null +++ b/scripts/apps/authoring-react/with-keybindings.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import {IKeyBindings} from 'superdesk-api'; + +interface IProps { + keyBindings: IKeyBindings; +} + +export class WithKeyBindings extends React.PureComponent { + constructor(props: IProps) { + super(props); + + this.handleKeyUp = this.handleKeyUp.bind(this); + } + + handleKeyUp(event: KeyboardEvent) { + const matchingKeyBinding: string | null = Object.keys(this.props.keyBindings).find((hotkey) => { + const split = hotkey.split('+'); + const altRequired = split.includes('alt'); + const shiftRequired = split.includes('shift'); + const ctrlRequired = split.includes('ctrl'); + + return (altRequired ? event.altKey : !event.altKey) + && (shiftRequired ? event.shiftKey : !event.shiftKey) + && (ctrlRequired ? event.ctrlKey : !event.ctrlKey) + && (split[split.length - 1] === event.key.toLowerCase()); + }); + + if (matchingKeyBinding != null) { + event.stopPropagation(); + this.props.keyBindings[matchingKeyBinding](); + } + } + + componentDidMount() { + window.addEventListener('keyup', this.handleKeyUp); + } + + componentWillUnmount(): void { + window.removeEventListener('keyup', this.handleKeyUp); + } + + render(): React.ReactNode { + return this.props.children; + } +} diff --git a/scripts/apps/authoring/authoring/directives/AuthoringTopbarDirective.ts b/scripts/apps/authoring/authoring/directives/AuthoringTopbarDirective.ts index e85ff85866..28ba1aa96a 100644 --- a/scripts/apps/authoring/authoring/directives/AuthoringTopbarDirective.ts +++ b/scripts/apps/authoring/authoring/directives/AuthoringTopbarDirective.ts @@ -25,11 +25,7 @@ export function AuthoringTopbarDirective( templateUrl: 'scripts/apps/authoring/views/authoring-topbar.html', link: function(scope) { function setActionsFromExtensions() { - scope.articleActionsFromExtensions = []; - - getArticleActionsFromExtensions(scope.item).then((articleActions) => { - scope.articleActionsFromExtensions = articleActions; - }); + scope.articleActionsFromExtensions = getArticleActionsFromExtensions(scope.item); } scope.additionalButtons = authoringWorkspace.authoringTopBarAdditionalButtons; diff --git a/scripts/apps/highlights/views/menu.html b/scripts/apps/highlights/views/menu.html index 1b1f1ed796..8a2d28d79e 100644 --- a/scripts/apps/highlights/views/menu.html +++ b/scripts/apps/highlights/views/menu.html @@ -4,4 +4,4 @@ sd-hotkey-options="{global: true}" sd-hotkey-callback="highlightsHotkey" dropdown sd-package-highlights-dropdown> - \ No newline at end of file + diff --git a/scripts/apps/monitoring/directives/ItemActionsMenu.ts b/scripts/apps/monitoring/directives/ItemActionsMenu.ts index b53cf353b8..64aa275cf2 100644 --- a/scripts/apps/monitoring/directives/ItemActionsMenu.ts +++ b/scripts/apps/monitoring/directives/ItemActionsMenu.ts @@ -96,105 +96,103 @@ export function ItemActionsMenu( function getActions(item: IArticle): void { scope.menuGroups = []; - getArticleActionsFromExtensions(item) - .then((actionsFromExtensions) => { - let intent = {action: 'list', type: getType(item)}; - let activitiesByGroupName: {[groupName: string]: Array} = {}; - - // group activities by `activity.group` - superdesk.findActivities(intent, item).forEach((activity: IActivity) => { - if (workflowService.isActionAllowed(scope.item, activity.action)) { - let group = activity.group ?? 'default'; - - if (activitiesByGroupName[group] == null) { - activitiesByGroupName[group] = []; - } - if (scope.allowedActions?.length > 0) { - if (scope.allowedActions.includes(activity._id)) { - activitiesByGroupName[group].push(activity); - } - } else { - activitiesByGroupName[group].push(activity); - } - } - }); + const actionsFromExtensions = getArticleActionsFromExtensions(item); + let intent = {action: 'list', type: getType(item)}; + let activitiesByGroupName: {[groupName: string]: Array} = {}; - let menuGroups: Array = []; - - // take default menu groups, add activities and push to `menuGroups` - getAuthoringMenuGroups().forEach((group) => { - if (activitiesByGroupName[group._id] && activitiesByGroupName[group._id].length > 0) { - menuGroups.push({ - _id: group._id, - label: group.label, - concate: group.concate, - actions: activitiesByGroupName[group._id] - .map((activity) => ({kind: 'activity-based', activity: activity})), - }); - } - }); + // group activities by `activity.group` + superdesk.findActivities(intent, item).forEach((activity: IActivity) => { + if (workflowService.isActionAllowed(scope.item, activity.action)) { + let group = activity.group ?? 'default'; - // go over `activitiesByGroupName` and add groups not present - // in default groups (getAuthoringMenuGroups) - Object.keys(activitiesByGroupName).forEach((groupName) => { - var existingGroup = getAuthoringMenuGroups().find((g) => g._id === groupName); - - if (!existingGroup) { - menuGroups.push({ - _id: groupName, - label: groupName, - actions: activitiesByGroupName[groupName] - .map((activity) => ({kind: 'activity-based', activity: activity})), - }); + if (activitiesByGroupName[group] == null) { + activitiesByGroupName[group] = []; + } + if (scope.allowedActions?.length > 0) { + if (scope.allowedActions.includes(activity._id)) { + activitiesByGroupName[group].push(activity); } + } else { + activitiesByGroupName[group].push(activity); + } + } + }); + + let menuGroups: Array = []; + + // take default menu groups, add activities and push to `menuGroups` + getAuthoringMenuGroups().forEach((group) => { + if (activitiesByGroupName[group._id] && activitiesByGroupName[group._id].length > 0) { + menuGroups.push({ + _id: group._id, + label: group.label, + concate: group.concate, + actions: activitiesByGroupName[group._id] + .map((activity) => ({kind: 'activity-based', activity: activity})), + }); + } + }); + + // go over `activitiesByGroupName` and add groups not present + // in default groups (getAuthoringMenuGroups) + Object.keys(activitiesByGroupName).forEach((groupName) => { + var existingGroup = getAuthoringMenuGroups().find((g) => g._id === groupName); + + if (!existingGroup) { + menuGroups.push({ + _id: groupName, + label: groupName, + actions: activitiesByGroupName[groupName] + .map((activity) => ({kind: 'activity-based', activity: activity})), }); + } + }); - // actions(except viewing an item) are not allowed for items in legal archive - if (item._type !== 'legal_archive' && scope.allowedActions == null) { - // handle actions from extensions - let extensionActionsByGroupName: {[groupName: string]: Array} = {}; + // actions(except viewing an item) are not allowed for items in legal archive + if (item._type !== 'legal_archive' && scope.allowedActions == null) { + // handle actions from extensions + let extensionActionsByGroupName: {[groupName: string]: Array} = {}; - for (const action of actionsFromExtensions) { - const name = action.groupId ?? 'default'; + for (const action of actionsFromExtensions) { + const name = action.groupId ?? 'default'; - if (extensionActionsByGroupName[name] == null) { - extensionActionsByGroupName[name] = []; - } + if (extensionActionsByGroupName[name] == null) { + extensionActionsByGroupName[name] = []; + } - extensionActionsByGroupName[name].push(action); + extensionActionsByGroupName[name].push(action); + } + + Object.keys(extensionActionsByGroupName).forEach((group) => { + const existingGroup = menuGroups.find((_group) => _group._id === group); + + if (existingGroup == null) { + menuGroups.push({ + _id: group, + label: group, + actions: extensionActionsByGroupName[group] + .map((articleAction) => ({ + kind: 'extension-action', + articleAction: articleAction, + })), + }); + } else { + if (existingGroup.actions == null) { + existingGroup.actions = []; } - Object.keys(extensionActionsByGroupName).forEach((group) => { - const existingGroup = menuGroups.find((_group) => _group._id === group); - - if (existingGroup == null) { - menuGroups.push({ - _id: group, - label: group, - actions: extensionActionsByGroupName[group] - .map((articleAction) => ({ - kind: 'extension-action', - articleAction: articleAction, - })), - }); - } else { - if (existingGroup.actions == null) { - existingGroup.actions = []; - } - - existingGroup.actions = existingGroup.actions.concat( - extensionActionsByGroupName[group] - .map((articleAction) => ({ - kind: 'extension-action', - articleAction: articleAction, - })), - ); - } - }); + existingGroup.actions = existingGroup.actions.concat( + extensionActionsByGroupName[group] + .map((articleAction) => ({ + kind: 'extension-action', + articleAction: articleAction, + })), + ); } - - scope.menuGroups = menuGroups; }); + } + + scope.menuGroups = menuGroups; } /** diff --git a/scripts/apps/search/components/actions-menu/MenuItems.tsx b/scripts/apps/search/components/actions-menu/MenuItems.tsx index d25c0a25ae..3d7d0600f5 100644 --- a/scripts/apps/search/components/actions-menu/MenuItems.tsx +++ b/scripts/apps/search/components/actions-menu/MenuItems.tsx @@ -64,10 +64,8 @@ export default class MenuItems extends React.Component { // actions(except viewing an item) are not allowed for items in legal archive if (this.props.item._type !== 'legal_archive') { - getArticleActionsFromExtensions(this.props.item).then((actions) => { - this.setState({ - actionsFromExtensions: actions, - }); + this.setState({ + actionsFromExtensions: getArticleActionsFromExtensions(this.props.item), }); } else { this.setState({ diff --git a/scripts/core/superdesk-api-helpers.tsx b/scripts/core/superdesk-api-helpers.tsx index a7c8242d46..b1d3b91d71 100644 --- a/scripts/core/superdesk-api-helpers.tsx +++ b/scripts/core/superdesk-api-helpers.tsx @@ -2,7 +2,7 @@ import {IArticle, IAuthoringAction, IExtensionActivationResult} from 'superdesk- import {flatMap} from 'lodash'; import {extensions} from 'appConfig'; -export function getArticleActionsFromExtensions(item: IArticle): Promise> { +export function getArticleActionsFromExtensions(item: IArticle): Array { const actionGetters : Array = flatMap( @@ -10,8 +10,7 @@ export function getArticleActionsFromExtensions(item: IArticle): Promise extension.activationResult.contributions?.entities?.article?.getActions ?? [], ); - return Promise.all(actionGetters.map((getPromise) => getPromise(item))) - .then((res) => { - return flatMap(res); - }); + return flatMap( + actionGetters.map((getAction) => getAction(item)), + ); } diff --git a/scripts/core/superdesk-api.d.ts b/scripts/core/superdesk-api.d.ts index 39ab1e847a..f9325aea10 100644 --- a/scripts/core/superdesk-api.d.ts +++ b/scripts/core/superdesk-api.d.ts @@ -162,6 +162,7 @@ declare module 'superdesk-api' { availableOffline: boolean; priority: IDisplayPriority; group: 'start' | 'middle' | 'end'; + keyBindings?: IKeyBindings; } interface IPropsAuthoring { @@ -178,7 +179,7 @@ declare module 'superdesk-api' { authoringStorage: IAuthoringStorage; storageAdapter: IStorageAdapter; fieldsAdapter: IFieldsAdapter; - getActions?(options: IExposedFromAuthoring): Promise>; // three dots menu actions + getActions?(options: IExposedFromAuthoring): Array; // three dots menu actions getInlineToolbarActions(options: IExposedFromAuthoring): IAuthoringOptions; getAuthoringTopBarWidgets( options: IExposedFromAuthoring, @@ -195,6 +196,8 @@ declare module 'superdesk-api' { onFieldChange?(fieldId: string, fieldsData: IFieldsData): IFieldsData; validateBeforeSaving?: boolean; // will block saving if invalid. defaults to true + + getSideWidgetNameAtIndex(item: T, index: number): string; } // AUTHORING-REACT FIELD TYPES - attachments @@ -430,12 +433,20 @@ declare module 'superdesk-api' { */ export type IDisplayPriority = number; + /** + * EXAMPLE: `{'ctrl+shift+s': () => save()}` + */ + export interface IKeyBindings { + [key: string]: () => void; + } + export interface IAuthoringAction { groupId?: string; // action lists can specify which groups they wanna render via an id priority?: IDisplayPriority; icon?: string; label: string; onTrigger(): void; + keyBindings?: IKeyBindings; } export interface IArticleActionBulk { @@ -623,7 +634,7 @@ declare module 'superdesk-api' { article: IArticle, contentProfile: IContentProfileV2, fieldsData: import('immutable').Map, - ): Promise>; + ): Array; mediaActions?: Array>; pages?: Array; @@ -637,7 +648,7 @@ declare module 'superdesk-api' { }; entities?: { article?: { - getActions?(article: IArticle): Promise>; + getActions?(article: IArticle): Array; getActionsBulk?(articles: Array): Promise>; onPatchBefore?(id: IArticle['_id'], patch: Partial, dangerousOptions?: IDangerousArticlePatchingOptions,): Promise>; // can alter patch(immutably), can cancel patching onSpike?(item: IArticle): Promise;