superdesk/superdesk-client-core

View on GitHub
scripts/apps/monitoring/controllers/AggregateCtrl.ts

Summary

Maintainability
F
5 days
Test Coverage
import {each, forEach, isNil, partition, keyBy} from 'lodash';
import {gettext, getItemTypes} from 'core/utils';
import {SCHEDULED_OUTPUT, DESK_OUTPUT} from 'apps/desks/constants';
import {appConfig} from 'appConfig';
import {IMonitoringFilter, IStage, IDesk, IMonitoringGroup} from 'superdesk-api';
import {getLabelForStage} from 'apps/workspace/content/constants';
import {getExtensionSections} from '../services/CardsService';

AggregateCtrl.$inject = ['$scope', 'desks', 'workspaces', 'preferencesService', 'storage',
    'savedSearch', 'content'];
export function AggregateCtrl($scope, desks, workspaces, preferencesService, storage,
    savedSearch, content) {
    const CONTENT_PROLFILE = gettext('Content profile');
    var PREFERENCES_KEY = 'agg:view';
    var defaultMaxItems = 10;
    var self = this;

    this.loading = true;
    this.selected = null;
    this.groups = [];
    this.spikeGroups = [];
    this.personalGroups = {
        'personal': {type: 'personal', header: gettext('Personal Items')},
        'sent': {type: 'sent', header: gettext('Sent Items')},
    };
    this.defaultPersonalGroup = {};
    this.modalActive = false;
    this.displayOnlyCurrentStep = false;
    this.columnsLimit = null;
    this.currentStep = 'desks';
    this.searchLookup = {};
    this.deskLookup = {};
    this.stageLookup = {};
    this.fileTypes = getItemTypes();
    this.monitoringSearch = false;
    this.searchQuery = null;
    this.isOutputType = desks.isOutputType;

    this.activeProfiles = [];
    this.activeFilters = {
        contentProfile: $scope.type === 'monitoring' ? storage.getItem('contentProfile') || [] : [],
        fileType: $scope.type === 'monitoring' ? storage.getItem('fileType') || [] : [],
        customFilters: $scope.type === 'monitoring' ? storage.getItem('customFilters') || {} : {},
    };
    this.activeFilterTags = {};

    const extensionSection = getExtensionSections();

    extensionSection.forEach((response) => {
        self.personalGroups[response.id] = {type: response.id, header: response.label, query: response.query};
    });

    function initPersonalGroup() {
        self.defaultPersonalGroup = self.personalGroups['personal'];
    }

    this.togglePersonalGroup = (id, group) => {
        self.defaultPersonalGroup.type = id;
    };

    const initializeDesksAndStages = () => {
        this.desks = desks.desks._items;
        this.deskLookup = desks.deskLookup;
        this.deskStages = desks.deskStages;
        each(this.desks, (desk) => {
            each(self.deskStages[desk._id], (stage) => {
                self.stageLookup[stage._id] = stage;
            });
        });
    };

    desks.initialize()
        .then(() => initializeDesksAndStages())
        .then(angular.bind(this, function() {
            return savedSearch.getAllSavedSearches().then(angular.bind(this, function(searchesList) {
                this.searches = searchesList;
                each(this.searches, (item) => {
                    self.searchLookup[item._id] = item;
                });
            }));
        }))
        .then(angular.bind(this, function() {
            return this.readSettings()
                .then(angular.bind(this, function(settings) {
                    initGroups(settings);
                    setupCards();
                    this.settings = settings;
                    getActiveProfiles();
                }));
        }));

    /**
     * If view showed as widget set the current widget
     *
     *@param {object} widget
     */
    this.setWidget = function(widget) {
        this.widget = widget;
    };

    /**
     * Read the setting for currently selected workspace(desk or custom workspace)
     * If the view is showed in a widget, read the settings from widget configuration.
     * If the view is showed in a desk settings, read the settings from desk's monitoring settings.
     * If the current selected workspace is a custom workspace then settings are read from user preferences.
     * If the current selected workspace is a desk then settings are read from user preferences first, if
     * no user preference found then settings read from desk's monitoring settings.
     * @returns {Object} promise - when resolved return the list of settings
     */
    this.readSettings = function() {
        if (self.widget) {
            // when reading from monitoring widget
            return widgetMonitoringConfig(self.widget);
        }

        return workspaces.getActiveId().then((activeWorkspace) => {
            if (!isNil(self.settings) && self.settings.desk) {
                // when viewing in desk's monitoring settings
                return deskSettingsMonitoringConfig(self.settings.desk);
            }
            // when viewing in monitoring view
            return workspaceMonitoringConfig(activeWorkspace);
        });
    };

    /**
     * Read settings in monitoring view for custom or desk workspace
     * according to active workspace type
     * @param {Object} activeWorkspace - contains workspace id and type.
     **/
    function workspaceMonitoringConfig(activeWorkspace) {
        if (activeWorkspace.type === 'workspace') {
            // when custom workspace selected in monitoring view.
            return customWorkspaceMonitoringConfig(activeWorkspace);
        } else if (activeWorkspace.type === 'desk') {
            // when desk selected in monitoring view.
            return deskWorkspaceMonitoringConfig(activeWorkspace);
        }
    }

    /**
     * 1. Start with monitoring preferences or desk stages
     * 2. Apply ordering from user preferences
     *
     * IMPORTANT:
     *  - if stages or saved searches are added they must appear regardless of user preferences
     *  - if stages or saved searches are removed, they must not appear regardless of user preferences
     */
    function deskWorkspaceMonitoringConfig(activeWorkspace): {type: string; groups: Array<IMonitoringGroup>} {
        return preferencesService.get(PREFERENCES_KEY).then((preference) => {
            let desk = self.deskLookup[activeWorkspace.id];
            let monitoringSettings = desk?.monitoring_settings;

            let activePrefGroups = preference[activeWorkspace.id] ? preference[activeWorkspace.id].groups || [] : [];
            let activePrefGroupKeys = activePrefGroups.map(({_id}) => _id);

            const groups = monitoringSettings ?? getDefaultGroups({});
            const groupsKeyed = keyBy(groups, (group) => group._id);

            const [inPrefs, notInPrefs] = partition(groups, (group) => groupsKeyed[group._id] != null);

            const inPrefsSorted = inPrefs.sort((group1, group2) => {
                const index1 = activePrefGroupKeys.indexOf(group1._id);
                const index2 = activePrefGroupKeys.indexOf(group2._id);

                return index1 - index2;
            });

            return {type: 'desk', groups: [...inPrefsSorted, ...notInPrefs]};
        });
    }

    /**
     * Read aggregate settings in monitoring view for custom workspace
     * @param {Object} activeWorkspace - contains workspace id and type.
     * @return {Object} {type: {String}, groups: {Array}}
     **/
    function customWorkspaceMonitoringConfig(activeWorkspace) {
        return preferencesService.get(PREFERENCES_KEY).then((preference) => {
            let groups = [];

            if (preference && preference[activeWorkspace.id] && preference[activeWorkspace.id].groups) {
                groups = preference[activeWorkspace.id].groups;
            }
            return {type: 'workspace', groups: groups};
        });
    }

    /**
     * Read aggregate settings for monitoring widget
     * @param {Object} objWidget - contains widget configuration
     * @return {Object} {type: {String}, groups: {Array}}
     **/
    function widgetMonitoringConfig(objWidget) {
        return workspaces.readActive().then((workspace) => {
            let groups = [];

            self.widget.configuration = objWidget.configuration || {groups: [], label: ''};
            each(workspace.widgets, (widget) => {
                if (widget.configuration && self.widget._id === widget._id
                    && self.widget.multiple_id === widget.multiple_id) {
                    groups = widget.configuration.groups || groups;
                    self.widget.configuration.label = widget.configuration.label || '';
                }
            });
            return {type: 'desk', groups: groups};
        });
    }

    /**
     * Read aggregate settings for desk monitoring settings
     * @param {Object} objDesk - contains desk configuration
     * @return {Object} {type: {String}, groups: {Array}, desk: {Object}}
     **/
    function deskSettingsMonitoringConfig(objDesk) {
        let groups = [];
        let desk = self.deskLookup[objDesk._id];

        if (desk && desk.monitoring_settings) {
            groups = desk.monitoring_settings;
        }
        return {type: 'desk', groups: groups, desk: objDesk};
    }

    /**
     * If not configured in desk > monitoring settings
     * only desk stages will be shown
     * global searches will not be shown
     */
    function getDefaultGroups(settings): Array<IMonitoringGroup> {
        const desk: IDesk = isNil(settings.desk) ? desks.getCurrentDesk() : settings.desk;

        const deskStages: Array<IStage> =
            Object.values(self.stageLookup)
                .filter((stage: IStage) => stage.desk === desk._id) as any as Array<IStage>;

        let defaultGroups: Array<IMonitoringGroup> = deskStages.map((stage) => ({
            _id: stage._id,
            type: 'stage',
            header: stage.name,
        }));

        defaultGroups.push({_id: desk._id + ':output', type: DESK_OUTPUT, header: desk.name});

        if (appConfig?.monitoring?.scheduled) {
            defaultGroups.push({
                _id: desk._id + ':scheduled',
                type: SCHEDULED_OUTPUT,
                header: desk.name,
            });
        }

        return defaultGroups;
    }

    /**
     * Init groups by filter out from groups stages or saved searches that
     * are not available(deleted or no right on them for stages only) and return all
     * stages for current desk if monitoring setting is not set
     **/
    function initGroups(settings) {
        self.groups =
            (settings.groups.length > 0 ? settings.groups : getDefaultGroups(settings))
                .filter((card) => {
                    if (card.type === 'stage') { // filter out deleted stages
                        return self.stageLookup[card._id] != null;
                    } else if (card.type === 'search') { // filter out deleted searches
                        return self.searchLookup[card._id] != null;
                    } else {
                        return true;
                    }
                });

        initSpikeGroups(settings.type === 'desk');
        initPersonalGroup();
        updateFilteringCriteria();
        self.search(self.searchQuery);
    }

    /**
     * Init the spike desks based on already initialized groups
     *
     * for desk workspace only show current desk spiked items
     *
     * @param {boolean} isDesk
     */
    function initSpikeGroups(isDesk) {
        var spikeDesks: any = {};

        if (self.spikeGroups.length > 0) {
            self.spikeGroups.length = 0;
        }

        if (isDesk) {
            var desk = desks.getCurrentDesk();

            if (desk) {
                self.spikeGroups = [{_id: desk._id, type: 'spike'}];
            }
            return;
        }

        if (self.groups.length === 0) {
            return;
        }

        each(self.groups, (item, index) => {
            if (item.type === 'stage') {
                var stage = self.stageLookup[item._id];

                spikeDesks[stage.desk] = self.deskLookup[stage.desk];
            } else if (item.type === 'personal') {
                spikeDesks.personal = {_id: 'personal', name: 'personal'};
            }
        });

        each(spikeDesks, (item: any) => {
            if (item._id === 'personal') {
                self.spikeGroups.push({_id: item._id, type: 'spike-personal', header: item.name});
            } else {
                self.spikeGroups.push({_id: item._id, type: 'spike', header: item.name});
            }
        });
    }

    /**
     * Refresh view after setup
     */
    function refresh() {
        if (self.loading) {
            return null;
        }

        /**
         * When a new stage is added, it should appear in the list of desk stages
         * in monitoring settings.
         */
        initializeDesksAndStages();

        return self.readSettings()
            .then((settings) => {
                initGroups(settings);
                setupCards();
                self.settings = settings;
            });
    }

    this.refreshGroups = refresh;

    /**
     * Read the settings when the current workspace
     * selection is changed
     */
    $scope.$watch(() => workspaces.active, refresh);

    /**
     * Return true if the 'fileType' filter is selected
     * param {string} fileType
     * @return boolean
     */
    this.hasFileType = function(fileType) {
        if (fileType === 'all') {
            return this.activeFilters.fileType.length === 0;
        }
        return this.activeFilters.fileType.includes(fileType);
    };

    this.getSelectedFileTypes = function(): string {
        return this.activeFilters.fileType.length === 0 ? null : JSON.stringify(this.activeFilters.fileType);
    };

    this.getSelectedContentProfiles = function(): string {
        return this.activeFilters.contentProfile.length === 0 ? null
            : JSON.stringify(this.activeFilters.contentProfile);
    };

    function updateFilteringCriteria() {
        forEach(self.activeFilters, (filterValue, filterType) => {
            var value = filterValue.length === 0 ? null : JSON.stringify(filterValue);

            each(self.groups, (item) => {
                item[filterType] = value;
            });
            each(self.spikeGroups, (item) => {
                item[filterType] = value;
            });

            self.defaultPersonalGroup[filterType] = value;
        });
    }

    this.isCustomFilterActive = (filter: IMonitoringFilter) => {
        return Object.keys(this.activeFilters.customFilters ?? {}).includes(filter.label);
    };

    this.toggleCustomFilter = (filter: IMonitoringFilter) => {
        if (typeof this.activeFilters.customFilters === 'undefined') {
            this.activeFilters.customFilters = {};
        }

        if (Object.keys(this.activeFilters.customFilters).includes(filter.label)) {
            delete this.activeFilters.customFilters[filter.label];
        } else {
            this.activeFilters.customFilters[filter.label] = filter;
        }

        updateFilterInStore();
        updateFilteringCriteria();
        $scope.$apply();
    };

    this.setCustomFilter = (filter: IMonitoringFilter) => {
        if (typeof this.activeFilters.customFilters === 'undefined') {
            this.activeFilters.customFilters = {};
        }

        this.activeFilters.customFilters[filter.label] = filter;

        updateFilterInStore();
        updateFilteringCriteria();
        $scope.$apply();
    };

    this.setFilterType = function(filterType, filterValue, $event?) {
        if (filterType === 'contentProfile') {
            if (!this.activeFilters.contentProfile.includes(filterValue._id)) {
                this.activeFilters.contentProfile.push(filterValue._id);
                const tag = {'key': filterValue._id, 'label': gettext(filterValue.label)};

                if (Array.isArray(this.activeFilterTags[CONTENT_PROLFILE])) {
                    this.activeFilterTags[CONTENT_PROLFILE].push(tag);
                } else {
                    this.activeFilterTags[CONTENT_PROLFILE] = [tag];
                }
            }
        } else if (filterType === 'file') {
            if (filterValue === 'all') {
                this.activeFilters.fileType = [];
            } else {
                let filterIndex = this.activeFilters.fileType.indexOf(filterValue);

                if (filterIndex > -1) {
                    this.activeFilters.fileType.splice(filterIndex, 1);
                } else {
                    this.activeFilters.fileType.push(filterValue);
                }
            }
        }

        updateFilterInStore();
        updateFilteringCriteria();

        if ($event?.ctrlKey) {
            $event.stopPropagation();
            return null;
        }
    };

    this.removeFilter = (filter, type?) => {
        if (filter == null) {
            this.activeFilters.contentProfile = [];
            this.activeFilterTags = {};
        } else {
            this.activeFilters.contentProfile = this.activeFilters.contentProfile
                .filter((profile) => profile !== filter);
            this.activeFilterTags[type] = this.activeFilterTags[type].filter((tags) => tags.key !== filter);
        }

        updateFilterInStore();
        updateFilteringCriteria();
    };

    // Save filters in the store
    function updateFilterInStore() {
        if ($scope.type === 'monitoring') {
            storage.setItem('fileType', self.activeFilters.fileType);
            storage.setItem('contentProfile', self.activeFilters.contentProfile);
            storage.setItem('customFilters', self.activeFilters.customFilters);
        }
    }

    /**
     * Add card metadata into current groups
     */
    function setupCards() {
        var cards = self.groups;

        angular.forEach(cards, setupCard);
        self.cards = cards;

        /**
         * Add card metadata into group
         */
        function setupCard(card) {
            if (card.type === 'stage') {
                var stage = self.stageLookup[card._id];
                var desk = self.deskLookup[stage.desk];

                card.deskId = stage.desk;
                card.header = desk.name;
                card.subheader = getLabelForStage(stage);
            } else if (desks.isOutputType(card.type)) {
                var deskId = card._id.substring(0, card._id.indexOf(':'));

                card.header = self.deskLookup[deskId].name;
            } else if (card.type === 'search') {
                card.search = self.searchLookup[card._id];
                card.header = card.search.name;
            } else if (card.type === 'personal') {
                card.header = gettext('Personal');
            }
        }
    }

    this.preview = function(item) {
        this.selected = item;
    };

    /**
     * For edit monitoring settings add desk groups to the list
     */
    this.edit = function(currentStep, displayOnlyCurrentStep) {
        this.editGroups = {};

        this.refreshGroups().then(() => {
            var _groups = this.groups;

            each(_groups, (item, index) => {
                self.editGroups[item._id] = {
                    _id: item._id,
                    selected: true,
                    type: item.type,
                    max_items: item.max_items || defaultMaxItems,
                    order: index,
                };
                if (item.type === 'stage') {
                    var stage = self.stageLookup[item._id];

                    self.editGroups[stage.desk] = {
                        _id: stage._id,
                        selected: true,
                        type: 'desk',
                        order: 0,
                    };
                } else if (desks.isOutputType(item.type)) {
                    var deskId = item._id.substring(0, item._id.indexOf(':'));

                    self.editGroups[deskId] = {
                        _id: item._id,
                        selected: true,
                        type: 'desk',
                        order: 0,
                    };
                }
            });
        });

        this.modalActive = true;

        this.currentStep = currentStep || 'desks';
        this.displayOnlyCurrentStep = displayOnlyCurrentStep;
    };

    this.searchOnEnter = function($event, query) {
        var ENTER = 13;

        if ($event.keyCode === ENTER) {
            this.search(query);
            $event.stopPropagation();
        }
    };

    /**
     * Set the search set by user on all groups
     */
    this.search = function(query) {
        this.searchQuery = query;
        each(this.groups, (item) => {
            item.query = query;
        });
        each(this.spikeGroups, (item) => {
            item.query = query;
        });
        self.defaultPersonalGroup.query = query;
    };

    this.state = storage.getItem('agg:state') || {};
    this.state.expanded = this.state.expanded || {};

    this.switchExpandedState = function(key) {
        this.state.expanded[key] = !this.getExpandedState(key);
        storage.setItem('agg:state', this.state);
    };

    this.getExpandedState = function(key) {
        return this.state.expanded[key] === undefined ? true : this.state.expanded[key];
    };

    this.setSoloGroup = function(group) {
        this.state.solo = group;
        storage.setItem('agg:state', this.state);
    };

    this.getMaxHeightStyle = function(maxItems) {
        var maxHeight = 32 * (maxItems || defaultMaxItems);

        return {'max-height': maxHeight.toString() + 'px'};
    };

    $scope.$on('open:archived_kill', (evt, item, action) => {
        $scope.archived_kill = item;
        $scope.archived_kill_action = action;
    });

    $scope.$on('open:resend', (evt, item) => {
        $scope.resend = item;
    });

    function getActiveProfiles() {
        content.getTypes('text', false).then((profiles) => {
            self.loading = false;
            self.activeProfiles = profiles;

            // initialize the activeFilterTags once the activeProfiles are available
            if (self.activeFilters.contentProfile.length > 0) {
                self.activeFilterTags[CONTENT_PROLFILE] = self.activeFilters.contentProfile.map((filter) => {
                    const profile = profiles.find((p) => p._id === filter);

                    return {key: profile._id, label: profile.label};
                });
            }
        });
    }
}