superdesk/superdesk-client-core

View on GitHub
scripts/apps/users/directives/UserPreferencesDirective.ts

Summary

Maintainability
F
4 days
Test Coverage
/* eslint-disable max-len */
/* tslint:disable:max-line-length */
import {gettext} from 'core/utils';
import {appConfig, getUserInterfaceLanguage} from 'appConfig';
import {applyDefault} from 'core/helpers/typescript-helpers';
import {DEFAULT_EDITOR_THEME} from 'apps/authoring/authoring/services/AuthoringThemesService';

const THEME_LIGHT = 'light-ui';

/**
 * @ngdoc directive
 * @module superdesk.apps.users
 * @name sdUserPreferences
 * @description
 *   This directive creates the Preferences tab on the user profile
 *   panel, allowing users to set various system preferences for
 *   themselves.
 */
UserPreferencesDirective.$inject = ['session', 'preferencesService', 'notify', 'asset',
    'metadata', 'desks', 'modal', '$timeout', '$q', 'userList', 'lodash', 'search', 'authThemes'];

export function UserPreferencesDirective(
    session, preferencesService, notify, asset, metadata, desks, modal,
    $timeout, $q, userList, _, search, authThemes,
) {
    // human readable labels for server values
    const LABELS = {
        mgrid: gettext('Grid View'),
        compact: gettext('List View'),
        photogrid: gettext('Photo Grid View'),
        list: gettext('List View'),
        swimlane: gettext('Swimlane View'),
    };

    const ICONS = {
        mgrid: 'grid-view',
        compact: 'list-view',
        photogrid: 'grid-view',
        list: 'list-view',
        swimlane: 'kanban-view',
    };

    return {
        templateUrl: asset.templateUrl('apps/users/views/user-preferences.html'),
        link: function(scope, element, attrs) {
            const userLang = getUserInterfaceLanguage().replace('_', '-');
            const body = angular.element('body');

            scope.activeNavigation = null;

            scope.activeTheme = localStorage.getItem('theme');

            /*
             * Set this to true after adding all the preferences to the scope. If done before, then the
             * directives which depend on scope variables might fail to load properly.
             */

            scope.preferencesLoaded = false;
            var orig; // original preferences, before any changes

            preferencesService.get(null, true).then((result) => {
                orig = result;
                buildPreferences(orig);

                scope.datelineSource = session.identity.dateline_source;
                scope.datelinePreview = scope.preferences['dateline:located'].located;
                scope.featurePreview = scope.preferences['feature:preview'];
            });

            scope.cancel = function() {
                scope.userPrefs.$setPristine();
                buildPreferences(orig);

                scope.datelinePreview = scope.preferences['dateline:located'].located;
            };

            scope.goTo = function(id) {
                document.getElementById(id).scrollIntoView({
                    behavior: 'smooth',
                });

                scope.activeNavigation = id;
            };

            scope.checkNavigation = function(id) {
                return scope.activeNavigation === id;
            };

            userList.getUser(scope.user._id, true).then((u) => {
                scope.user = u;
            });

            /**
            * Saves the preferences changes on the server. It also
            * invokes additional checks beforehand, namely the
            * preferred categories selection.
            *
            * @method save
            */
            scope.save = function() {
                preSaveCategoriesCheck()
                    .then(() => {
                        var update = createPatchObject();

                        return preferencesService.update(update).then(() => {
                            userList.getUser(scope.user._id, true).then((u) => {
                                scope.user = u;
                            });
                            return update;
                        });
                    }, () => $q.reject('canceledByModal'))
                    .then((preferences) => {
                    // ask for browser permission if desktop notification is enable
                        if (_.get(preferences, 'desktop:notification.enabled')) {
                            preferencesService.desktopNotification.requestPermission();
                        }

                        localStorage.setItem('theme', scope.activeTheme);
                        body.attr('data-theme', scope.activeTheme);
                        notify.success(gettext('User preferences saved'));
                        scope.cancel();
                    }, (reason) => {
                        if (reason !== 'canceledByModal') {
                            notify.error(gettext(
                                'User preferences could not be saved...',
                            ));
                        }
                    });
            };

            /**
             * Invoked by the directive after updating the property in item. This method is responsible for updating
             * the properties dependent on dateline.
             */
            scope.changeDatelinePreview = function(datelinePreference, city) {
                if (city === '') {
                    datelinePreference.located = null;
                }

                $timeout(() => {
                    scope.datelinePreview = datelinePreference.located;
                });
            };

            /**
            * Marks all categories in the preferred categories list
            * as selected.
            *
            * @method checkAll
            */
            scope.checkAll = function() {
                scope.categories.forEach((cat) => {
                    cat.selected = true;
                });
                scope.userPrefs.$setDirty();
            };

            /**
            * Marks all categories in the preferred categories list
            * as *not* selected.
            *
            * @method checkNone
            */
            scope.checkNone = function() {
                scope.categories.forEach((cat) => {
                    cat.selected = false;
                });
                scope.userPrefs.$setDirty();
            };

            /**
            * Marks the categories in the preferred categories list
            * that are considered default as selected, and all the
            * other categories as *not* selected.
            *
            * @method checkDefault
            */
            scope.checkDefault = function() {
                scope.categories.forEach((cat) => {
                    cat.selected = !!scope.defaultCategories[cat.qcode];
                });
                scope.userPrefs.$setDirty();
            };

            /**
             * Sets the form as dirty when value is changed. This function should be used when one wants to set
             * form dirty for input controls created without using <input>.
             *
             * @method articleDefaultsChanged
             */
            scope.articleDefaultsChanged = function(item) {
                scope.userPrefs.$setDirty();
            };

            scope.showCategory = function(preference) {
                if (preference.category === 'rows') {
                    return appConfig.list != null && appConfig.list.singleLineView;
                }
                const noShowCategories = [
                    'article_defaults',
                    'categories',
                    'desks',
                    'notifications',
                    'planning',
                    'cvs',
                ];

                return _.indexOf(noShowCategories, preference.category) < 0;
            };

            scope.profileConfig = applyDefault(appConfig.profile, {});

            /**
             * Determine if the planning related preferences should be shown based on the existance of the
             * Agenda endpoint.
             *
             * @method showPlanning
             */
            scope.showPlanning = function() {
                return !angular.isUndefined(scope.features.agenda);
            };

            scope.valueLabel = (value) => LABELS[value] || value;

            scope.getIcon = (value) => ICONS[value] || 'list-view';

            /**
            * Builds a user preferences object in scope from the given
            * data.
            *
            * @function buildPreferences
            * @param {Object} data - user preferences data, arranged in
            *   logical groups. The keys represent these groups' names,
            *   while the corresponding values are objects containing
            *   user preferences settings for a particular group.
            */
            function buildPreferences(data) {
                var buckets, // names of the needed metadata buckets
                    initNeeded; // metadata service init needed?

                scope.preferences = {};
                _.each(data, (val, key) => {
                    if (val.label && val.category) {
                        scope.preferences[key] = _.create(val);
                    }
                });

                // metadata service initialization is needed if its
                // values object is undefined or any of the needed
                // data buckets are missing in it
                buckets = [
                    'categories',
                    'default_categories',
                    'locators',
                    'calendars',
                    'agendas',
                    'eventsPlanningFilters',
                ];

                initNeeded = buckets.some((bucketName) => {
                    var values = metadata.values || {};

                    return angular.isUndefined(values[bucketName]);
                });

                if (initNeeded) {
                    var initPromises = [];

                    initPromises.push(metadata.initialize(), desks.initialize());
                    $q.all(initPromises).then(() => {
                        updateScopeData(metadata.values, data);
                    });
                } else {
                    updateScopeData(metadata.values, data);
                }
            }

            /**
            * Updates auxiliary scope data, such as the lists of
            * available and content categories to choose from.
            *
            * @function updateScopeData
            * @param {Object} helperData - auxiliary data used by the
            *   preferences settings UI
            * @param {Object} userPrefs - user's personal preferences
            *   settings
            */
            function updateScopeData(helperData, userPrefs) {
                // If the planning module is installed we save a list of the available agendas
                if (scope.features.agenda) {
                    scope.agendas = helperData.agendas;
                }

                // If the planning module is installed we save a list of the available events_planning_filters
                if (scope.features.events_planning_filters) {
                    scope.eventsPlanningFilters = helperData.eventsPlanningFilters;
                }

                // A list of category codes that are considered
                // preferred by default, unless of course the user
                // changes this preference setting.
                scope.defaultCategories = {};
                if (Array.isArray(helperData.default_categories)) {
                    helperData.default_categories.forEach((cat) => {
                        scope.defaultCategories[cat.qcode] = true;
                    });
                }

                // Create a list of categories for the UI widgets to
                // work on. New category objects are created so that
                // objects in the existing category list are protected
                // from modifications on ng-model changes.
                scope.categories = [];
                helperData.categories.forEach((cat) => {
                    var newObj = _.create(cat),
                        selectedCats = userPrefs['categories:preferred'].selected;

                    newObj.selected = !!selectedCats[cat.qcode];
                    if (cat.translations?.name?.[userLang]) {
                        newObj.name = cat.translations.name[userLang];
                    }
                    scope.categories.push(newObj);
                });

                /**
                 * @ngdoc property
                 * @name sdUserPreferences#desks
                 * @type {array}
                 * @private
                 * @description Create a list of desks for the UI widgets to
                 * work on. New desk objects are created so that
                 * objects in the existing desk list are protected
                 * from modifications on ng-model changes.
                 */
                scope.desks = [];
                _.each(desks.deskLookup, (desk) => {
                    var newObj = _.create(desk),
                        selectedDesks = userPrefs['desks:preferred'].selected;

                    newObj.selected = !!selectedDesks[desk._id];
                    scope.desks.push(newObj);
                });

                scope.locators = helperData.locators;

                if (scope.preferences['singleline:view']) {
                    search.updateSingleLineStatus(scope.preferences['singleline:view'].enabled);
                }

                scope.calendars = helperData.event_calendars;

                scope.preferencesLoaded = true;
            }

            /**
            * Checks if at least one preferred category has been
            * selected, and if not, asks the user whether or not to
            * proceed with a default set of categories selected.
            *
            * Returns a promise that is resolved if saving the
            * preferences should continue, and rejected if it should be
            * aborted (e.g. when no categories are selected AND the
            * user does not confirm using a default set of categories).
            *
            * @function preSaveCategoriesCheck
            * @return {Object} - a promise object
            */
            function preSaveCategoriesCheck() {
                var modalResult,
                    msg,
                    someSelected;

                someSelected = scope.categories.some((cat) => cat.selected);

                if (someSelected) {
                    // all good, simply return a promise that resolves
                    return $q.when();
                }

                msg = gettext('No preferred categories selected. Should you choose to proceed with your choice. A default set of categories will be selected for you.');

                modalResult = modal.confirm(msg).then(() => {
                    scope.checkDefault();
                });

                return modalResult;
            }

            /**
            * Creates and returns a user preferences object that can
            * be used as a parameter in a PATCH request to the server
            * when user preferences are saved.
            *
            * @function createPatchObject
            * @return {Object}
            */
            function createPatchObject() {
                var p = {};

                _.each(orig, (val, key) => {
                    if (key === 'dateline:located') {
                        var $input = element.find('.input-term > input');

                        scope.changeDatelinePreview(scope.preferences[key], $input[0].value);
                    }

                    if (key === 'categories:preferred') {
                        val.selected = {};
                        scope.categories.forEach((cat) => {
                            val.selected[cat.qcode] = !!cat.selected;
                        });
                    }

                    if (key === 'desks:preferred') {
                        val.selected = {};
                        scope.desks.forEach((desk) => {
                            val.selected[desk._id] = !!desk.selected;
                        });
                    }

                    p[key] = _.extend(val, scope.preferences[key]);
                });

                if (orig['editor:theme'] != null) {
                    p['editor:theme'] = {
                        ...orig['editor:theme'],
                        theme: JSON.stringify(authThemes.syncWithApplicationTheme(
                            scope.activeTheme,
                            orig['editor:theme'].theme.length
                                ? orig['editor:theme'].theme
                                : JSON.stringify(DEFAULT_EDITOR_THEME.theme),
                        )),
                    };
                }

                return p;
            }
        },
    };
}