scripts/core/activity/activity.ts
import {forEach} from 'lodash';
import langmap from 'core/lang';
import {gettext} from 'core/utils';
import {dashboardRoute, appConfig} from 'appConfig';
import {IActivity} from 'superdesk-interfaces/Activity';
var constants = {
MENU_MAIN: 'superdesk.core.menu.main',
MENU_SETTINGS: 'superdesk.core.menu.settings',
ACTION_EDIT: 'edit',
ACTION_LIST: 'list',
ACTION_VIEW: 'view',
ACTION_PREVIEW: 'preview',
};
// using a function with no arguments is required so gettext
// is not called before angular starts and translations are loaded
export const coreMenuGroups = {
WORKFLOW: {
id: 'WORKFLOW',
priority: -500,
getLabel: () => gettext('Workflow'),
},
CONTENT_CONFIG: {
id: 'CONTENT_CONFIG',
priority: -800,
getLabel: () => gettext('Content config'),
},
CONTENT_FLOW: {
id: 'CONTENT_FLOW',
priority: -200,
getLabel: () => gettext('Content flow'),
},
};
/**
* @ngdoc provider
* @module superdesk.core.activity
* @name superdeskProvider
* @requires $routeProvider
* @requires lodash
* @description The superdesk provider exposes an API for registering new
* application components, such as activities, widgets, etc.
*/
SuperdeskProvider.$inject = ['$routeProvider', 'lodash'];
function SuperdeskProvider($routeProvider, _) {
var widgets = {};
var activities: any = {};
var permissions = {};
var panes = {};
angular.extend(this, constants);
/**
* @ngdoc method
* @name superdeskProvider#widget
* @public
* @param {string} id
* @param {Object} data
* @returns {Object} self
* @description Register widget.
*/
this.widget = function(id, data) {
widgets[id] = angular.extend({_id: id, wcode: id}, data);
return this;
};
/**
* @ngdoc method
* @name superdeskProvider#pane
* @public
* @description Register a pane.
*/
this.pane = function(key, data) {
panes[key] = angular.extend({_id: key}, data);
return this;
};
// Register a new activity.
this.activity = function(id, activityData: IActivity) {
var activity = angular.extend({
_id: id,
priority: 0,
when: id, // use id as default
href: id, // use id as default
filters: [],
beta: false,
reloadOnSearch: false,
auth: true,
features: {},
privileges: {},
condition: function(item) {
return true;
},
}, activityData);
var actionless = _.find(activity.filters, (filter) => !filter.action);
if (actionless) {
console.error('Missing filters action for activity', activity);
}
if (activity.when[0] === '/' && (activity.template || activity.templateUrl)) {
$routeProvider.when(activity.when, activity);
}
activities[id] = activity;
return this;
};
/**
* @ngdoc method
* @name superdeskProvider#permission
* @public
*
* @param {string} id
* @param {Object} data
* @returns {Object} self
*
* @description Register permission.
*/
this.permission = function(id, data) {
permissions[id] = angular.extend({_id: id}, data);
return this;
};
/**
* @ngdoc service
* @module superdesk.core.activity
* @name superdesk
* @requires $q
* @requires $route
* @requires $rootScope
* @requires activityService
* @requires activityChoose
* @requires betaService
* @requires features
* @requires privileges
* @requires $injector
* @requires lodash
* @requires config
* @description This service allows interacting with registered activities.
*/
this.$get = ['$q', '$rootScope', 'activityService', 'activityChooser',
'betaService', 'features', 'privileges', '$injector',
function superdeskFactory($q, $rootScope, activityService, activityChooser, betaService,
features, privileges, $injector) {
/**
* Render main menu depending on registered acitivites
*/
betaService.isBeta().then((beta) => {
forEach(activities, (activity, id) => {
if (activity.beta === true && !beta || !isAllowed(activity)) {
$routeProvider.when(activity.when, {redirectTo: dashboardRoute});
}
});
});
/**
* Let user to choose an activity
*/
function chooseActivity(_activities) {
return activityChooser.choose(_activities);
}
function checkFeatures(activity) {
var isMatch = true;
angular.forEach(activity.features, (val, key) => {
isMatch = isMatch && features[key] && val;
});
return isMatch;
}
function checkPrivileges(activity) {
return privileges.userHasPrivileges(activity.privileges);
}
function checkActivityEnabled(activity) {
if (!appConfig.activity) {
return true;
}
if (_.isUndefined(appConfig.activity[activity._id])) {
return true;
}
return appConfig.activity[activity._id];
}
/**
* @ngdoc method
* @name superdesk#isAllowed
* @private
*
* @param {Object} activity
*
* @description Test if user is allowed to use given activity.
* Testing is based on current server setup (features) and user privileges.
*/
function isAllowed(activity) {
return checkActivityEnabled(activity) && checkFeatures(activity) && checkPrivileges(activity);
}
return angular.extend({
widgets: widgets,
activities: activities,
permissions: permissions,
panes: panes,
/**
* @ngdoc method
* @name superdesk#activity
* @public
* @description Return activity by given id
*/
activity: function(id) {
return activities[id] || null;
},
/**
* @ngdoc method
* @name superdesk#resolve
* @public
* @description Resolve an intent to a single activity
*/
resolve: function(intent) {
var _activities = this.findActivities(intent);
switch (_activities.length) {
case 0:
return $q.reject();
case 1:
return $q.when(_activities[0]);
default:
return chooseActivity(_activities);
}
},
/**
* @ngdoc method
* @name superdesk#findActivities
* @public
* @description
* Find all available activities for given intent
*/
findActivities: function(intent, item) {
var criteria: any = {};
if (intent.action) {
criteria.action = intent.action;
}
if (intent.type) {
criteria.type = intent.type;
}
if (intent.id) {
criteria.id = intent.id;
}
return _.sortBy(_.filter(this.activities, (activity) => {
return _.find(activity.filters, criteria) && isAllowed(activity) &&
activity.condition(item) && testAdditionalCondition();
function testAdditionalCondition() {
if (activity.additionalCondition) {
return $injector.invoke(
activity.additionalCondition,
{},
{item: item ? item : intent.data},
);
}
return true;
}
}), 'priority').reverse();
},
/**
* @ngdoc method
* @name superdesk#intent
* @param {string} action
* @param {string} type
* @param {Object} data
* @returns {Object} promise
* @public
* @description
* Starts an activity for given action and data
*/
intent: function(action, type, data, id) {
var intent = {
action: action,
type: type,
data: data,
id: id,
};
var self = this;
return this.resolve(intent).then((activity) => self.start(activity, intent), () => {
$rootScope.$broadcast([
'intent',
intent.action || '*',
intent.type || '*',
].join(':'), intent);
return $q.reject();
});
},
/**
* @ngdoc method
* @name superdesk#link
*
* @param {string} activity
* @param {Object} data
* @returns {string}
*
* @description
* Get a link for given activity
*/
link: function getSuperdeskLink(activity, data) {
return activityService.getLink(this.activity(activity), data);
},
/**
* @ngdoc method
* @name superdesk#start
*
* @param {Object} activity
* @param {Object} locals
* @return {Promise}
*
* @description Start activity
*
*/
start: function(activity, locals) {
return activityService.start(activity, locals);
},
/**
* @ngdoc method
* @name superdesk#getMenu
*
* @param {string} category
*
* @description
* Get activities based on menu category
*/
getMenu: function getMenu(category) {
return privileges.loaded.then(() => {
var menu = [];
angular.forEach(activities, (activity) => {
if (activity.category === category &&
isAllowed(activity) &&
(activity.beta === false || $rootScope.beta) &&
(activity.additionalCondition == null || $injector.invoke(activity.additionalCondition))
) {
menu.push(activity);
}
});
return menu;
});
},
}, constants);
}];
}
/**
* @ngdoc module
* @module superdesk.core.activity
* @name superdesk.core.activity
* @packageName superdesk.core
* @description Superdesk core activities module. Used to register new activities,
* apps and functionalities.
*/
angular.module('superdesk.core.activity', [
'ngRoute',
'superdesk.core.notify',
'superdesk.core.features',
'superdesk.core.translate',
'superdesk.core.services.beta',
'superdesk.core.services.modal',
'superdesk.core.privileges',
'superdesk.core.keyboard',
'superdesk.core.activity.chooser',
'superdesk.core.activity.list',
'superdesk.core.activity.modal',
])
.constant('lodash', window._)
.constant('langmap', langmap)
.provider('superdesk', SuperdeskProvider)
/**
* @ngdoc service
* @module superdesk.core.activity
* @name activityService
* @requires $location
* @requires $injector
* @requires $q
* @requires modal
* @requires lodash
* @description The service allows choosing activities to perform.
*/
.service('activityService', ['$location', '$injector', '$q', 'modal', 'lodash',
function ActivityService($location, $injector, $q, modal, _) {
var activityStack = [];
this.activityStack = activityStack;
/**
* Expand path using given locals, eg. with /users/:Id and locals {Id: 2} returns /users/2
*
* @param {Object} activity
* @param {Object} locals
* @returns {string}
*/
function getPath(activity, locals) {
if (activity.href[0] === '/') { // trigger route
var matchAll = true,
path = activity.href.replace(/:([_a-zA-Z0-9]+)/, (match, key) => {
matchAll = matchAll && locals[key];
return locals[key] ? locals[key] : match;
});
path = matchAll ? path : null;
if (activity.href.indexOf('_type') !== -1 && !_.isNull(path)) {
path = path.replace(':_type', locals._type ? locals._type : 'archive');
}
return path;
}
}
/**
* @ngdoc method
* @name activityService#getLink
* @public
*
* @param {Object} activity
* @param {Object} locals
* @returns {string}
*
* @description
* Get URL for given activity
*/
this.getLink = getPath;
/**
* @ngdoc method
* @name activityService#start
* @public
*
* @param {object} activity
* @param {object} locals
* @returns {object} promise
*
* @description
* Start given activity
*/
this.start = function startActivity(activity, locals) {
function execute(_activity, _locals) {
var path = getPath(_activity, _locals && _locals.data);
if (path) { // trigger route
$location.path(path);
return $q.when(_locals);
}
if (_activity.modal) {
var defer = $q.defer();
activityStack.push({
defer: defer,
activity: _activity,
locals: _locals,
});
return defer.promise;
}
return $q.when($injector.invoke(_activity.controller, {}, _locals));
}
if (activity.confirm) {
return modal.confirm(gettext(activity.confirm)).then(function runConfirmed() {
return execute(activity, locals);
}, () => $q.reject({confirm: 1}));
}
return execute(activity, locals);
};
}])
.run(['$rootScope', 'superdesk', function($rootScope, superdesk) {
$rootScope.superdesk = superdesk; // add superdesk reference so we can use constants in templates
$rootScope.intent = function(...args) {
return superdesk.intent(...args);
};
$rootScope.link = function(...args) {
var path = superdesk.link(...args);
return path ? '#' + path : null;
};
}])
/**
* @ngdoc service
* @module superdesk.core.activity
* @name activityChooser
* @description
* Activity chooser service - bridge between superdesk and activity chooser directive
*/
.service('activityChooser', ['$q', function($q) {
var defer;
this.choose = function(activities) {
defer = $q.defer();
this.activities = activities;
return defer.promise;
};
this.resolve = function(activity) {
this.activities = null;
defer.resolve(activity);
};
this.reject = function() {
this.activities = null;
defer.reject();
};
}])
/**
* @ngdoc service
* @module superdesk.core.activity
* @name referrer
* @description
* Referrer service to set/get the referrer Url
*/
.service('referrer', ['lodash', function(_) {
/**
* @ngdoc method
* @name referrer#setReferrer
* @public
*
* @param {Object} currentRoute
* @param {Object} previousRoute
* @returns {string}
*
* @description
* Serving for the purpose of setting referrer url via referrer service, also
* setting url in localStorage. which is utilized to get last working screen
* on authoring page if referrer url is unidentified direct link
* (i.e from notification pane)
*/
this.setReferrer = function(currentRoute, previousRoute) {
if (currentRoute && previousRoute) {
if (currentRoute.$$route !== undefined && previousRoute.$$route !== undefined) {
if (currentRoute.$$route.originalPath === '/') {
this.setReferrerUrl(dashboardRoute);
localStorage.setItem('referrerUrl', dashboardRoute);
sessionStorage.removeItem('previewUrl');
} else if (currentRoute.$$route.authoring && (!previousRoute.$$route.authoring ||
previousRoute.$$route._id === 'packaging')) {
this.setReferrerUrl(prepareUrl(previousRoute));
localStorage.setItem('referrerUrl', this.getReferrerUrl());
sessionStorage.removeItem('previewUrl');
}
}
}
};
var referrerURL;
this.setReferrerUrl = function(refURL) {
referrerURL = refURL;
};
this.getReferrerUrl = function() {
if (typeof referrerURL === 'undefined' || referrerURL === null) {
if (typeof localStorage.getItem('referrerUrl') === 'undefined'
|| localStorage.getItem('referrerUrl') === null) {
this.setReferrerUrl(dashboardRoute);
} else {
referrerURL = localStorage.getItem('referrerUrl');
}
}
return referrerURL;
};
/**
* @ngdoc method
* @name referrer#prepareUrl
* @private
*
* @param {Object} refRoute
* @returns {string}
*
* @description
* Prepares complete Referrer Url from previous route href and querystring params(if exist),
* e.g /workspace/content?q=test$repo=archive
*/
function prepareUrl(refRoute) {
var completeUrl;
if (refRoute) {
completeUrl = refRoute.$$route.href.replace('/:_id', '');
if (!_.isEqual({}, refRoute.pathParams)) {
completeUrl = completeUrl + '/' + refRoute.pathParams._id;
}
if (!_.isEqual({}, refRoute.params)) {
completeUrl = completeUrl + '?';
completeUrl = completeUrl + decodeURIComponent($.param(refRoute.params));
}
}
return completeUrl;
}
}])
// reject modal on route change
// todo(petr): what about blocking route change as long as it is opened?
.run(['$rootScope', 'activityService', 'referrer', function($rootScope, activityService, referrer) {
$rootScope.$on('$routeChangeStart', () => {
if (activityService.activityStack.length) {
var item = activityService.activityStack.pop();
item.defer.reject();
}
});
$rootScope.$on('$routeChangeSuccess', (ev, currentRoute, previousRoute) => {
referrer.setReferrer(currentRoute, previousRoute);
});
}])
.directive('sdActivityItem', ActivityItemDirective)
.directive('sdActivityDropdownItem', ActivityItemDropdownDirective);
ActivityItemDirective.$inject = ['asset'];
function ActivityItemDirective(asset) {
return {
templateUrl: asset.templateUrl('core/activity/views/activity-item.html'),
};
}
ActivityItemDropdownDirective.$inject = ['asset'];
function ActivityItemDropdownDirective(asset) {
return {
templateUrl: asset.templateUrl('core/activity/views/activity-dropdown-item.html'),
link: function(scope, elem, attr) {
scope.group = attr.group;
},
};
}
export interface IActivityService {
activityStack: Array<{activity: IActivity, defer: any, locals: any}>;
getLink(activity: IActivity, locals: any): string;
start(activity: IActivity, locals: any): Promise<any>;
}