superdesk/superdesk-client-core

View on GitHub
scripts/apps/monitoring/directives/WidgetGroup.ts

Summary

Maintainability
F
6 days
Test Coverage
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';

import {WidgetItemList as WidgetItemListComponent} from 'apps/search/components';
import {IActivityService} from 'core/activity/activity';

WidgetGroup.$inject = [
    'search',
    'api',
    'superdesk',
    'desks',
    'cards',
    '$timeout',
    '$q',
    '$location',
    '$anchorScroll',
    'activityService',
    '$rootScope',
    'datetime',
    'metadata',
];

export function WidgetGroup(search, api, superdesk, desks, cards, $timeout, $q,
    $location, $anchorScroll, activityService: IActivityService, $rootScope, datetime, metadata) {
    const services = {
        $anchorScroll: $anchorScroll,
        $location: $location,
        $q: $q,
        $rootScope: $rootScope,
        $timeout: $timeout,
        activityService: activityService,
        api: api,
        cards: cards,
        datetime: datetime,
        desks: desks,
        metadata: metadata,
        search: search,
        superdesk: superdesk,
    };

    return {
        scope: {
            stage: '=',
            total: '=',
            canEdit: '=',
            showEmpty: '=?',
            maxItems: '=?',
            selected: '=?',
            action: '&',
            filter: '=',
        },
        link: function(scope, elem) {
            var criteria;

            scope.page = 1;
            scope.fetching = false;
            scope.itemIds = [];
            scope.itemsById = {};

            /**
             * Generates Identifier to be used by track by expression.
             */
            scope.generateTrackByIdentifier = function(item) {
                return search.generateTrackByIdentifier(item);
            };

            scope.preview = function(item) {
                superdesk.intent('preview', 'item', item);
            };

            scope.edit = function(item) {
                if (item._type === 'ingest') {
                    var activity = superdesk.findActivities({action: 'list', type: 'ingest'}, item)[0];

                    activityService.start(activity, {data: {item: item}}).then((_item) => {
                        initEdit(_item);
                    });
                } else {
                    initEdit(item);
                }

                function initEdit(_item) {
                    superdesk.intent('edit', 'item', _item).then(null, () => {
                        superdesk.intent('view', 'item', _item);
                    });
                }
            };

            function getProvider(_criteria) {
                var provider = 'archive';

                if (scope.stage.type && (desks.isOutputType(scope.stage.type) || scope.stage.type === 'search')) {
                    provider = 'search';
                }

                if (_criteria.repo && _criteria.repo.indexOf(',') === -1) {
                    provider = _criteria.repo;
                }

                return provider;
            }

            function queryItems(queryString?, params?) {
                if (!scope.fetching) {
                    // page reload disabled when the user scrolls
                    if (container.scrollTop > 20) {
                        return;
                    }
                    scope.page = 1;
                    scope.itemIds = [];
                    scope.itemsById = {};
                }

                criteria = cards.criteria(scope.stage, queryString);

                if (scope.page > 0 && criteria.source) {
                    criteria.source.from = (scope.page - 1) * criteria.source.size;
                }

                if (params) {
                    angular.extend(criteria, params);
                }

                api.query(getProvider(criteria), criteria)
                    .then((items) => {
                        items._items.forEach((item) => {
                            var itemId = search.generateTrackByIdentifier(item);

                            if (!scope.itemsById[itemId]) {
                                scope.itemIds.push(itemId);
                            }
                            scope.itemsById[itemId] = item;
                        });
                        scope.total = items._meta.total;
                    })
                    .finally(() => {
                        scope.fetching = false;

                        if ($rootScope.config.features.customMonitoringWidget &&
                            !scope.selected && scope.itemIds && scope.itemIds.length) {
                            scope.selected = scope.itemsById[(_.head(scope.itemIds) as string)];
                            scope.action({item: scope.selected});
                        }

                        scope.updateList({
                            itemIds: scope.itemIds,
                            itemsById: scope.itemsById,
                            loading: false,
                            selected: scope.selected,
                        });
                    });
            }

            scope.updateItem = function(item, gone, schedule) {
                var itemId;

                if (!item) {
                    if (schedule) {
                        scheduleQuery();
                        return;
                    }
                    return;
                }

                itemId = search.generateTrackByIdentifier(item);

                if (scope.itemsById[itemId]) {
                    angular.extend(item, {
                        gone: gone,
                    });
                    scope.itemsById[itemId] = item;
                    scope.updateList({
                        itemIds: scope.itemIds,
                        itemsById: scope.itemsById,
                    });
                } else if (schedule) {
                    scheduleQuery();
                }
            };

            scope.$on('item:spike', scheduleQuery);
            scope.$on('item:unspike', scheduleQuery);
            scope.$on('item:copy', scheduleQuery);
            scope.$on('item:unlink', scheduleQuery);
            scope.$on('item:duplicate', scheduleQuery);
            scope.$on('item:translate', scheduleQuery);
            scope.$on('item:highlights', scheduleQuery);
            scope.$on('item:marked_desks', scheduleQuery);

            if (scope.stage.type === 'search' && search.doesSearchAgainstRepo(scope.stage.search, 'ingest')) {
                scope.$on('ingest:update', scheduleQuery);
            }

            scope.$watch('filter', (query) => {
                container.scrollTop = 0;
                queryItems(query);
            });

            scope.$on('task:stage', (_e, data) => {
                if (scope.stage && (data.new_stage === scope.stage._id || data.old_stage === scope.stage._id)) {
                    scope.updateItem(getItem(data.item), data.new_stage !== scope.stage._id, true);
                }
            });

            scope.$on('content:update', (_e, data) => {
                if (data && cards.shouldUpdate(scope.stage, data)) {
                    scheduleQuery();
                }
            });

            scope.$on('item:fetch', (_e, data) => {
                if (cards.shouldUpdate(scope.stage, data)) {
                    scheduleQuery();
                }
            });

            scope.$on('item:move', (_e, data) => {
                if (data.to_desk && data.from_desk !== data.to_desk ||
                    data.to_stage && data.from_stage !== data.to_stage) {
                    scope.updateItem(getItem(data.item), scope.stage._id !== data.to_stage, true);
                    scheduleQuery(500);
                }
            });

            scope.$on('content:expired', (_e, data) => {
                scope.updateItem(getItem(data.item), true, false);
            });

            scope.$on('item:lock', (_e, data) => {
                var item = getItem(data.item);

                if (!item) {
                    return;
                }
                item.lock_user = data.user;
                scope.updateItem(item, false, false);
            });

            scope.$on('item:unlock', (_e, data) => {
                var item = getItem(data.item);

                if (!item) {
                    return;
                }
                item.lock_user = null;
                scope.updateItem(item, false, false);
            });

            function getItem(itemId) {
                var result;

                _.forOwn(scope.itemsById, (item, key) => {
                    if (item._id === itemId) {
                        result = item;
                    }
                });

                return result;
            }

            var queryTimeout;

            /**
             * Schedule content reload after some delay
             *
             * In case it gets called multiple times it will query only once
             */
            function scheduleQuery(delay = 5000) {
                if (!queryTimeout) {
                    queryTimeout = $timeout(() => {
                        queryItems(null, {auto: 1});
                        scope.$applyAsync(() => {
                            // ignore any updates requested in current $digest
                            queryTimeout = null;
                        });
                    }, delay, false);
                }
            }

            var container = elem[0];
            var offsetY = 0;
            var itemHeight = 0;
            var lastScrollTop = 0;

            elem.bind('scroll', (event) => {
                if (scope.fetching) { // ignore scrolling while fetching
                    event.preventDefault();
                    return false;
                }

                if (container.scrollTop + container.offsetHeight >= container.scrollHeight - 3 &&
                    lastScrollTop <= container.scrollTop
                ) {
                    lastScrollTop = container.scrollTop;
                    return scope.fetchNext().then(() => {
                        setFetching();
                        container.scrollTop -= 3;
                    });
                }

                if (container.scrollTop <= 2 && lastScrollTop >= container.scrollTop) {
                    lastScrollTop = container.scrollTop;
                    offsetY = 2 - container.scrollTop;
                    container.scrollTop += offsetY;
                }
            });

            /**
             * This will ignore scrolling for a while, used before we trigger scrolling
             */
            function setFetching() {
                scope.fetching = true;
                $timeout(() => {
                    scope.$applyAsync(() => {
                        scope.fetching = false;
                    });
                }, 200, false);
            }

            scope.fetchNext = function() {
                if (!scope.fetching) {
                    scope.page += 1;
                    scope.fetching = true;
                    queryItems();
                }

                return $q.when(false);
            };

            var UP = -1,
                DOWN = 1;

            var code;

            elem.on('keydown', (e) => {
                scope.$apply(() => {
                    if (e.keyCode) {
                        code = e.keyCode;
                    } else if (e.which) {
                        code = e.which;
                    }
                    if (code === 38) {
                        scope.move(UP, e);
                    }
                    if (code === 40) {
                        scope.move(DOWN, e);
                    }
                    if (code === 13 && scope.selected) {
                        scope.edit(scope.selected);
                    }
                });
            });

            scope.move = function(diff, event) {
                if (!_.isNil(scope.selected) && $rootScope.config.features.customMonitoringWidget && scope.itemIds) {
                    var itemId = scope.generateTrackByIdentifier(scope.selected);
                    var index = scope.itemIds.findIndex((x) => x === itemId);

                    if (!itemHeight) {
                        var containerItems = container.getElementsByTagName('li');

                        if (containerItems.length) {
                            itemHeight = containerItems[0].offsetHeight;
                        }
                    }
                    if (index === -1) { // selected not in current items, select first
                        container.scrollTop = 0;
                        clickItem(scope.itemsById[(_.head(scope.itemIds) as any)], event);
                    }
                    var nextIndex = _.max([0, _.min([scope.itemIds.length - 1, index + diff])]);

                    if (nextIndex < 0) {
                        clickItem(scope.itemsById[(_.last(scope.itemIds) as any)], event);
                    }
                    if (index !== nextIndex) {
                        // scrolling in monitoring widget for ntb is done by keyboard
                        // when we select next item if item is out of focus (not visible) it will scroll down
                        if ((nextIndex + 2) * itemHeight > container.offsetHeight + container.scrollTop &&
                            nextIndex > index) {
                            container.scrollTop += itemHeight * 2;
                        }
                        // when we select previous item if item is out of focus (not visible) it will scroll up
                        if (nextIndex * itemHeight < container.scrollTop && nextIndex < index) {
                            container.scrollTop -= itemHeight * 2;
                        }
                        clickItem(scope.itemsById[scope.itemIds[nextIndex]], event);
                    } else if (event) {
                        event.preventDefault();
                        event.stopPropagation();
                        event.stopImmediatePropagation();
                    }
                }
            };

            function clickItem(item, $event) {
                scope.select(item, false);
                if ($event) {
                    $event.preventDefault();
                    $event.stopPropagation();
                    $event.stopImmediatePropagation();
                }
            }

            scope.select = function(item, apply = true) {
                scope.action({item: item});
                scope.updateList({selected: item});
                if (apply) {
                    scope.$apply();
                }
            };

            scope.setLoading = function(loading) {
                scope.updateList({
                    loading: loading,
                });
            };

            scope.updateList = function(listProps) {
                scope.updateListProps = {
                    ...scope.updateListProps,
                    ...listProps,
                };
                var itemList = React.createElement(WidgetItemListComponent, scope.updateListProps);

                ReactDOM.render(itemList, elem[0]);
            };

            scope.updateListProps = {
                canEdit: scope.canEdit,
                customMonitoringWidget: $rootScope.config.features.customMonitoringWidget,
                svc: services,
                select: scope.select,
                edit: scope.edit,
            };
            scope.updateList({});

            // remove react elem on destroy
            scope.$on('$destroy', () => {
                elem.off();
                ReactDOM.unmountComponentAtNode(elem[0]);
            });
        },
    };
}