superdesk/superdesk-client-core

View on GitHub
scripts/core/activity/activity.ts

Summary

Maintainability
F
3 days
Test Coverage
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>;
}