scripts/apps/authoring/macros/macros.ts
/**
* @ngdoc service
* @module superdesk.apps.authoring.macros
* @name macros
* @requires api
* @requires notify
* @description MacrosService provides set of methods which allows fetching of macros
* and triggering of macros to be apply on provided item
*/
import {gettext} from 'core/utils';
import _, {debounce, once} from 'lodash';
interface IMacro {
name: string;
replace_type?: 'no-replace' | 'simple-replace' | 'keep-style-replace' | 'editor_state';
}
MacrosService.$inject = ['api', 'notify'];
function MacrosService(api, notify) {
/**
* Recursively returns all macros
*
* @return {*}
*/
var _getAllMacros = function(criteria = {}, page = 1, macros = []) {
return api.query('macros', _.extend({max_results: 200, page: page}, criteria), null, true)
.then((result) => {
let all = macros.concat(result._items);
let pg = page;
if (result._links.next) {
pg++;
return _getAllMacros(criteria, pg, all);
}
return _.sortBy(all, ['order', 'label']);
});
};
/**
* Returns all frontend macros if includeBackend is false or none
* If includeBackend is true then results will include the backend macros
*
* @param {bool} includeBackend
*/
this.get = function(includeBackend) {
return _getAllMacros({backend: !!includeBackend}).then(angular.bind(this, function(macros) {
this.macros = macros;
return this.macros;
}));
};
/**
* Returns all frontend macros for a given desk if includeBackend is false or none
* If includeBackend is true then results will include the backend macros for that desk
*
* @param {string} desk
* @param {bool} includeBackend
*/
this.getByDesk = function(desk, includeBackend) {
return _getAllMacros({desk: desk, backend: !!includeBackend}).then(angular.bind(this, function(macros) {
this.macros = macros;
return this.macros;
}));
};
this.setupShortcuts = function($scope) {
this.get().then((macros) => {
angular.forEach(macros, (macro) => {
if (macro.shortcut) {
$scope.$on('key:ctrl:' + macro.shortcut, () => {
triggerMacro(macro, $scope.item);
});
}
});
});
};
this.call = triggerMacro;
function triggerMacro(macro, item, commit?) {
return api.save('macros', {
macro: macro.name,
item: item,
commit: !!commit,
}).then((res) => res, (err) => {
if (angular.isDefined(err.data._message)) {
const error_messages = JSON.parse(err.data._message);
error_messages.forEach((error_message) => {
notify.error(error_message);
});
}
});
}
}
/**
* @ngdoc controller
* @module superdesk.apps.authoring.macros
* @name Macros
* @requires https://docs.angularjs.org/api/ng/type/$rootScope.Scope $scope
* @requires macros
* @requires desks
* @requires autosave
* @requires https://docs.angularjs.org/api/ng/service/$rootScope $rootScope
* @requires storage
* @requires editorResolver
* @description MacrosController holds a set of convenience functions used by macros widget
*/
MacrosController.$inject = ['$scope', 'macros', 'desks', 'autosave', '$rootScope', 'storage', 'editorResolver'];
function MacrosController($scope, macros, desks, autosave, $rootScope, storage, editorResolver) {
let expandedGroup = storage.getItem('expandedGroup') || [];
$scope.loading = true;
macros.get().then(() => {
let currentDeskId = desks.getCurrentDeskId();
if (currentDeskId !== null) {
macros.getByDesk(desks.getCurrentDesk().name).then((_macros) => {
displayMacros(_macros);
});
} else {
displayMacros(macros.macros);
}
})
.finally(() => {
$scope.loading = false;
});
/**
* @ngdoc method
* @name Macros#displayMacros
* @private
* @param {Array<object>} macros - each of macro contains name, label, group, order etc.
* @description displays the list of fetched macros
*/
function displayMacros(fetchedMacros) {
$scope.macros = fetchedMacros;
// grouped macros list
let macrosByGroup = _.groupBy(_.filter($scope.macros, 'group'), 'group');
$scope.groupedMacros = _.isEmpty(macrosByGroup) ? null : macrosByGroup;
// provide grouping macros list option and prepare list, if group available.
if ($scope.groupedMacros) {
$scope.groupedList = true;
prepareMacrosList($scope.macros);
} else {
$scope.groupedList = false;
}
}
function isString(value) {
return typeof value === 'string' || value instanceof String;
}
/**
* @ngdoc method
* @name Macros#call
* @param {Object} macro - contains name, label, group, order etc.
* @returns {Promise} - If resolved then macro is applied successfully
* @description
* Triggers macros service call to apply the provided macro on opened article.
* The macros that changes the body should return diff; for other fields the entire
* content is replaced with the value changed by macro
*/
$scope.call = function(macro: IMacro) {
const editor = editorResolver.get();
const isEditor3 = editor.version() === '3';
const useReplace = macro.replace_type === 'simple-replace' || macro.replace_type === 'keep-style-replace';
const isSimpleReplace = macro.replace_type === 'simple-replace';
let item = _.extend({}, $scope.origItem, $scope.item);
if (isEditor3 && useReplace) {
item.body_html = editor.getHtmlForTansa() || item.body_html;
}
$scope.loading = true;
return macros.call(macro, item).then((res) => {
let ignoreFields = ['_etag', 'fields_meta'];
// ignore fields is only required for editor3
if (isEditor3) {
if (macro.replace_type === 'editor_state') {
Object.keys(res.item.fields_meta).forEach((field) => {
editor.setEditorStateFromItem(res.item, field);
});
} else {
ignoreFields.push('body_html');
if (res.diff == null && useReplace === true && item.body_html !== res.item.body_html) {
editor.setHtmlFromTansa(res.item.body_html, isSimpleReplace);
}
Object.keys(res.item || {}).forEach((field) => {
if (isString(res.item[field]) === false || field === 'body_html') {
return;
}
ignoreFields.push(field);
if (res.item[field] !== item[field]) {
$rootScope.$broadcast('macro:refreshField', field, res.item[field]);
}
});
}
}
if (isEditor3 || res.diff == null) {
angular.extend($scope.item, _.omit(res.item, ignoreFields));
$scope.autosave($scope.item);
}
if (res.diff != null) {
$rootScope.$broadcast('macro:diff', res.diff);
}
$scope.closeWidget();
})
.finally(() => {
$scope.loading = false;
});
};
/**
* @ngdoc method
* @name Macros#prepareMacrosList
* @private
* @param {Array<object>} macros - each of macro contains name, label, group, order etc.
* @description Prepares sections for list of macros, which includes quick, grouped and
* miscellaneous set of macros for display
*/
function prepareMacrosList(allMacros) {
// macros quick list, i.e. where order is defined
$scope.quickList = _.filter(allMacros, 'order');
// miscellaneous macros list, i.e, where group is not defined
$scope.miscMacros = _.filter(allMacros, (o) => o.group === undefined);
// sort grouped macros
let ordered = {};
Object.keys($scope.groupedMacros)
.sort()
.forEach((key) => {
ordered[key] = _.sortBy($scope.groupedMacros[key], 'label');
});
// sorted grouped macros
$scope.orderedGroupedMacros = ordered;
}
/**
* @ngdoc method
* @name Macros#getGroupStatus
* @param {String} group - key that represents macro group, like area, currency etc.
* @returns {Boolean}
* @description gets the remembered toggle status of provided group,
* retrieved from local storage
*/
$scope.getGroupStatus = function(group) {
return _.includes(expandedGroup, group);
};
/**
* @ngdoc method
* @name Macros#setGroupStatus
* @param {String} group - key that represents macro group, like area, currency etc.
* @description sets the toggle status of provided group to be remember in local storage
*/
$scope.setGroupStatus = function(group) {
let index = expandedGroup.indexOf(group);
if (index > -1) {
expandedGroup.splice(index, 1);
} else {
expandedGroup.push(group);
}
storage.setItem('expandedGroup', expandedGroup);
};
}
/**
* @ngdoc directive
* @module superdesk.apps.authoring.macros
* @name sdMacrosReplace
* @requires editor
* @description sd-macro-replace performs the necessary replacement on editor's item in order to
* apply the results of triggered macro with the use of available set of methods such that next,
* prev and replace
*/
MacrosReplaceDirective.$inject = ['editorResolver'];
function MacrosReplaceDirective(editorResolver) {
return {
scope: true,
templateUrl: 'scripts/apps/authoring/macros/views/macros-replace.html',
link: function(scope) {
scope.diff = null;
// this is triggered from MacrosController.call and apply the changes to body field
scope.$on('macro:diff', (evt, diff) => {
scope.diff = diff;
init(scope.diff);
});
function init(diff) {
const editor = editorResolver.get();
if (diff) {
scope.noMatch = Object.keys(diff || {}).length;
editor.setSettings({findreplace: {diff: diff}});
editor.render();
editor.selectNext();
scope.preview = getCurrentReplace();
} else {
editor.setSettings({findreplace: null});
editor.render();
}
}
scope.next = function() {
const editor = editorResolver.get();
editor.selectNext();
scope.preview = getCurrentReplace();
};
scope.prev = function() {
const editor = editorResolver.get();
editor.selectPrev();
scope.preview = getCurrentReplace();
};
scope.replace = function() {
const editor = editorResolver.get();
var to = getCurrentReplace();
if (to) {
editor.replace(to);
editor.selectNext();
}
scope.preview = getCurrentReplace();
};
scope.close = function() {
scope.diff = null;
init(scope.diff);
};
function getCurrentReplace() {
const editor = editorResolver.get();
var from = editor.getActiveText();
return scope.diff[from] || null;
}
// There may be multiple instances of editors. Try waiting for all.
const initializeMacros = debounce(once(() => {
init(scope.diff);
scope.$apply();
}), 500);
window.addEventListener('editorInitialized', initializeMacros);
scope.$on('$destroy', () => {
window.removeEventListener('editorInitialized', initializeMacros);
});
},
};
}
/**
* @ngdoc module
* @module superdesk.apps.authoring.macros
* @name superdesk.apps.authoring.macros
* @packageName superdesk.apps
* @description Superdesk module that allows managing and using macros
*/
angular.module('superdesk.apps.authoring.macros', [
'superdesk.core.api',
'superdesk.core.notify',
'superdesk.apps.authoring.widgets',
'superdesk.apps.authoring.autosave',
])
.service('macros', MacrosService)
.controller('Macros', MacrosController)
.directive('sdMacrosReplace', MacrosReplaceDirective)
.config(['authoringWidgetsProvider', function(authoringWidgetsProvider) {
authoringWidgetsProvider
.widget('macros', {
icon: 'macro',
label: gettext('Macros'),
template: 'scripts/apps/authoring/macros/views/macros-widget.html',
order: 6,
needEditable: true,
side: 'right',
display: {
authoring: true,
packages: true,
killedItem: false,
legalArchive: false,
archived: false,
picture: true,
personal: true,
},
});
}]);