superdesk/superdesk-client-core

View on GitHub
scripts/apps/search/directives/SearchResults.ts

Summary

Maintainability
F
1 wk
Test Coverage
import _, {debounce, noop} from 'lodash';
import {gettext} from 'core/utils';
import {appConfig} from 'appConfig';
import {ISearchOptions, showRefresh} from '../services/SearchService';
import {IPackagesService} from 'types/Services/Packages';

SearchResults.$inject = [
    '$location',
    'preferencesService',
    'packages',
    'asset',
    '$timeout',
    'api',
    'search',
    'session',
    '$rootScope',
    'superdeskFlags',
    'notify',
    '$q',
];

const HEX_REG_EXP = /[a-f0-9]{24}/;

function isObjectId(value) {
    return value != null && value.length === 24 && HEX_REG_EXP.test(value);
}

/**
 * @ngdoc directive
 * @module superdesk.apps.search
 * @name sdSearchResults
 *
 * @requires $location
 * @requires preferencesService
 * @requires packages
 * @requires asset
 * @requires $timeout
 * @requires api
 * @requires search
 * @requires session
 * @requires $rootScope
 *
 * @description Item list with sidebar preview
 */
export function SearchResults(
    $location,
    preferencesService,
    packages: IPackagesService,
    asset,
    $timeout,
    api,
    search,
    session,
    $rootScope,
    superdeskFlags,
    notify,
    $q,
) { // uff - should it use injector instead?
    var preferencesUpdate = {
        'archive:view': {
            allowed: [
                'mgrid',
                'compact',
            ],
            category: 'archive',
            view: 'mgrid',
            default: 'mgrid',
            label: 'Users archive view format',
            type: 'string',
        },
    };

    return {
        require: '^sdSearchContainer',
        templateUrl: asset.templateUrl('apps/search/views/search-results.html'),
        link: function(scope, elem, attr, controller) {
            var containerElem = elem.find('.shadow-list-holder');
            var GRID_VIEW = 'mgrid',
                LIST_VIEW = 'compact';

            var projections = search.getProjectedFields();
            var multiSelectable = attr.multiSelectable !== undefined;

            scope.previewingBroadcast = false;
            superdeskFlags.flags.previewing = false;

            // allow controller overwrite getSearch
            const getSearch = typeof scope.search.getSearch === 'function'
                ? scope.search.getSearch
                : () => $location.search();

            const queryOptions: ISearchOptions = {
                hidePreviousVersions: scope.search.hideNested === true,
            };

            var criteria = search.query(getSearch(), queryOptions).getCriteria(true),
                oldQuery = _.omit(getSearch(), '_id');

            let fetchInProgress = false;

            /**
             * When a new request is initialized, all existing requests that are still in progress need to be cancelled.
             */
            let cancelAllBeforeTime: ReturnType<DateConstructor['now']> = 0;

            /**
             * Schedule an update
             */
            const queryItems = debounce(
                (event, data) => {
                    if (isObjectId(scope.search.repo.search) && event != null) {
                        // external provider, don't refresh on events
                        return;
                    }

                    if (scope.search.repo.search !== 'local' && !getSearch().q && !(data && data.force)) {
                        return; // ignore updates with external content
                    }

                    if (fetchInProgress) {
                        // schedule querying again after current query finishes
                        queryItems(event, data);
                        return;
                    }

                    scope.loading = true;
                    fetchInProgress = true;

                    _queryItems(data, Date.now())
                        .catch(() => noop)
                        .finally(() => {
                            scope.$applyAsync(() => {
                                fetchInProgress = false;
                            });
                        });
                },
                1000,
                {
                    leading: true,
                    trailing: true,
                    maxWait: 3000,
                },
            );

            scope.flags = controller.flags;
            scope.selected = scope.selected || {};
            scope.showHistoryTab = true;

            scope.context = 'search';
            scope.$on('item:deleted:archived', itemDelete);
            scope.$on('item:fetch', queryItems);
            scope.$on('item:update', updateItem);
            scope.$on('item:deleted', scheduleIfShouldUpdate);
            scope.$on('item:unlink', queryItems);
            scope.$on('item:spike', scheduleIfShouldUpdate);
            scope.$on('item:unspike', scheduleIfShouldUpdate);
            scope.$on('item:duplicate', queryItems);
            scope.$on('item:translate', queryItems);
            scope.$on('item:marked_desks', queryItems);

            scope.autorefreshContent = appConfig.features.autorefreshContent === true;

            // used by superdesk-fi
            scope.showtags = attr.showtags !== 'false';

            // Custom components for item fields
            scope.customRender = scope.search.customRender || {};

            scope.$on('ingest:update', (event, args) => {
                if (!scope.showRefresh) {
                    queryItems(event, args);
                }
            });

            scope.$on('content:update', queryItems);
            scope.$on('item:move', scheduleIfShouldUpdate);

            scope.$on('aggregations:changed', queryItems);

            scope.$on('broadcast:preview', (event, args) => {
                scope.previewingBroadcast = true;
                scope.preview(args.item);
            });

            scope.$on('broadcast:created', (event, args) => {
                scope.previewingBroadcast = true;
                queryItems(undefined, undefined);
                scope.preview(args.item);
            });

            scope.$watch('selected', (newVal, oldVal) => {
                if (!newVal && scope.previewingBroadcast) {
                    scope.previewingBroadcast = false;
                }
            });

            scope.$watch(function getSearchParams() {
                return _.omit(getSearch(), ['_id', 'item', 'action']);
            }, (newValue, oldValue) => {
                if (newValue !== oldValue) {
                    scope.refreshList();
                }
            }, true);

            // public api - called by list when needed
            scope.fetchNext = function() {
                render(null, true);
            };

            function updateItem(e, data) {
                var item = _.find(scope.items._items, {_id: data.item._id});

                if (item) {
                    angular.extend(item, data.item);
                }
            }

            /**
             * Function for fetching total items and filling scope.
             */
            function _queryItems(data: any | undefined, initiatedAt: ReturnType<DateConstructor['now']>) {
                const pageSize: number = 50;

                criteria = search.query(getSearch(), queryOptions).getCriteria(true);
                criteria.source.size = pageSize;
                var originalQuery;

                // when forced refresh or query then keep query size default as set 50 above.
                if (!(data && data.force)) {
                    // To compare current scope of items, consider fetching same number of items.
                    if (scope.items && scope.items._items.length > 50) {
                        criteria.source.size = scope.items._items.length;
                    }
                }

                if (data && (data.item || data.items || data.item_id) && scope.showRefresh && !data.force) {
                    // if we know the ids of the items then try to fetch those only
                    originalQuery = angular.extend({}, criteria.source.query);

                    let items = data.items || {};

                    if (data.item || data.item_id) {
                        items[data.item || data.item_id] = 1;
                    }

                    criteria.source.query = search.getItemQuery(data.items);
                } else {
                    criteria.aggregations = $rootScope.aggregations;
                }

                criteria.source.from = 0;
                scope.total = null;
                criteria.es_highlight = search.getElasticHighlight();
                criteria.projections = JSON.stringify(projections);

                if (getSearch().params) {
                    criteria.params = JSON.parse(getSearch().params);
                }

                return api.query(getProvider(criteria), criteria).then((items) => {
                    if (initiatedAt < cancelAllBeforeTime) {
                        return $q.reject('cancelling in favor of a newer request');
                    }

                    if (appConfig.features.autorefreshContent && data != null) {
                        data.force = true;
                    }

                    if (!scope.showRefresh && data && !data.force && data.user !== session.identity._id) {
                        scope.showRefresh =
                            items._items.length === pageSize
                                ? showRefresh(
                                    (scope.items?._items ?? []),
                                    items._items,
                                )
                                : false;
                    }

                    if (!scope.showRefresh || data && data.force) {
                        scope.total = items._meta.total;
                        scope.$applyAsync(() => {
                            render(items, null, true);
                        });
                    } else {
                        // update scope items only with the matching fetched items
                        scope.items = search.updateItems(items, scope.items);
                    }

                    if (originalQuery) {
                        criteria.source.query = originalQuery;
                    }
                }, (error) => {
                    notify.error(gettext('Failed to run the query!'));
                    console.error(error, getProvider(criteria));
                })
                    .finally(() => {
                        scope.loading = false;
                    });
            }

            function scheduleIfShouldUpdate(event, data) {
                if (data && data.item && _.includes(['item:spike', 'item:unspike', 'item:deleted'], event.name)) {
                    // item was spiked/unspikes from the list
                    extendItem(data.item, {
                        gone: true,
                        _etag: data.item,
                    });
                    queryItems(event, data);
                } else if (data && data.from_stage) {
                    // item was moved from current stage
                    extendItem(data.item, {
                        gone: true,
                        _etag: data.from_stage, // this must change to make it re-render
                    });
                    queryItems(event, data);
                }
            }

            function extendItem(itemId, updates) {
                scope.$apply(() => {
                    scope.items._items = scope.items._items.map((item) => {
                        if (item._id === itemId) {
                            return angular.extend(item, updates);
                        }

                        return item;
                    });

                    scope.items = angular.extend({}, scope.items); // trigger a watch
                });
            }

            scope.$on('refresh:list', (event, group, options) => {
                /**
                 * When manual refreshing is enabled, scrolling should not automatically refresh the list.
                 */
                if (scope.showRefresh === true && options?.event_origin === 'scroll') {
                    return;
                }

                scope.refreshList();
            });

            scope.refreshList = function() {
                scope.showRefresh = false;
                scope.manualRefreshInProgress = true;

                fetchInProgress = true;
                cancelAllBeforeTime = Date.now();

                _queryItems({force: true}, cancelAllBeforeTime)
                    .catch(noop)
                    .finally(() => {
                        fetchInProgress = false;
                        scope.manualRefreshInProgress = false;
                    });

                if (containerElem[0].scrollTop > 0) {
                    containerElem[0].scrollTop = 0;
                }
            };

            /*
             * Function to get the search endpoint name based on the criteria
             *
             * @param {Object} _criteria
             * @returns {string}
             */
            function getProvider(_criteria) {
                var provider = 'search';

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

                if (scope.search.repo.search && scope.search.repo.search !== 'local') {
                    provider = scope.search.repo.search;
                }

                if (isObjectId(provider)) {
                    _criteria.repo = provider;
                    provider = 'search_providers_proxy';
                }

                if (provider === 'local') {
                    provider = 'search';
                }

                return provider;
            }

            /*
             * Function for fetching the elements from the database
             *
             * @param {items}
             */
            function render(items, next, force?) {
                scope.loading = true;
                if (items) {
                    setScopeItems(items, force);
                } else if (next) {
                    scope.loading = true;
                    criteria.source.from = (criteria.source.from || 0) + criteria.source.size;
                    criteria.source.size = 50;
                    criteria.projections = JSON.stringify(projections);

                    api
                        .query(getProvider(criteria), criteria)
                        .then(setScopeItems)
                        .finally(() => {
                            scope.loading = false;
                        });
                } else {
                    var query = _.omit(getSearch(), '_id');

                    if (!_.isEqual(_.omit(query, 'page'), _.omit(oldQuery, 'page'))) {
                        $location.search('page', null);
                    }

                    criteria = search.query(getSearch(), queryOptions).getCriteria(true);
                    criteria.source.from = 0;
                    criteria.source.size = 50;
                    criteria.aggregations = $rootScope.aggregations;
                    criteria.es_highlight = search.getElasticHighlight();
                    scope.loading = true;
                    criteria.projections = JSON.stringify(projections);

                    api
                        .query(getProvider(criteria), criteria)
                        .then(setScopeItems)
                        .finally(() => {
                            scope.loading = false;
                        });
                    oldQuery = query;
                }

                function setScopeItems(_items, _force) {
                    scope.items = search.mergeItems(_items, scope.items, next, _force);
                    scope.total_records = _items._meta.total;
                    scope.loading = false;
                }
            }

            /*
             * Function for updating list
             * after item has been deleted
             */
            function itemDelete(e, data) {
                if (session.identity._id === data.user) {
                    queryItems(undefined, undefined);
                }
            }

            scope.preview = function preview(item) {
                if (multiSelectable) {
                    if (_.findIndex(scope.selectedList, {_id: item._id}) === -1) {
                        scope.selectedList.push(item);
                    } else {
                        _.remove(scope.selectedList, {_id: item._id});
                    }
                }

                if (item) {
                    if (item._type === 'externalsource') {
                        var preview_id = item.extra?.previewid;

                        if (preview_id) {
                            criteria.params = {'preview_id': preview_id};
                            return api.query(getProvider(criteria), criteria).then((item_) => {
                                if ('_items' in item_ && item_._items[0] !== undefined) {
                                    processPreview(search.mergeHighlightFields(item_._items[0]));
                                } else {
                                    processPreview(item);
                                }
                            }, (error) => {
                                notify.error('Detailed informations is not found for the item : ' + item.guid);
                                processPreview(item);
                            });
                        } else {
                            processPreview(item);
                            return;
                        }
                    }

                    scope.loading = true;
                    let previewCriteria = search.getSingleItemCriteria(item);

                    api.query(getProvider(previewCriteria), previewCriteria).then((completeItems) => {
                        let completeItem = search.mergeHighlightFields(completeItems._items[0]);

                        processPreview(completeItem);
                    })
                        .finally(() => {
                            scope.loading = false;
                        });
                } else {
                    delete scope.selected.preview;
                    superdeskFlags.flags.previewing = false;
                    sendRowViewEvents();
                }
            };

            /**
             * @ngdoc method
             * @name sdSearchResults#sendRowViewEvents
             * @private
             * @param {Object} item
             * @description If singLine:view preference is set, an item is being previewed, config has narrowView list
             * then, sends rowview event
             */
            function sendRowViewEvents(item?) {
                let sendEvent = scope.singleLine
                    && superdeskFlags.flags.authoring
                    && appConfig.list != null
                    && appConfig.list.narrowView;

                let evnt = item ? 'rowview:narrow' : 'rowview:default';

                if (sendEvent) {
                    $rootScope.$broadcast(evnt);
                }
            }

            /**
             * @ngdoc method
             * @name sdSearchResults#processPreview
             * @private
             * @param {Object} item
             * @description Sets the preview item
             */
            function processPreview(item) {
                scope.selected.preview = item;

                if (!_.isNil(scope.selected.preview)) {
                    scope.showHistoryTab = scope.selected.preview.state !== 'ingested' &&
                    !_.includes(['archived', 'externalsource'], scope.selected.preview._type);

                    superdeskFlags.flags.previewing = true;
                    sendRowViewEvents(item);
                }

                $location.search('_id', item ? item._id : null);
            }

            scope.openSingleItem = function(packageItem) {
                packages.fetchItem(packageItem).then((item) => {
                    scope.selected.view = item;
                });
            };

            scope.setview = setView;

            var savedView;

            preferencesService.get('archive:view').then((result) => {
                savedView = result.view;
                scope.view = !!savedView && savedView !== 'undefined' ? savedView : 'mgrid';
            });

            scope.$on('key:v', toggleView);

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

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

            function setView(view) {
                scope.view = view || 'mgrid';
                preferencesUpdate['archive:view'].view = view || 'mgrid';
                preferencesService.update(preferencesUpdate, 'archive:view');
            }

            function toggleView() {
                var nextView = scope.view === LIST_VIEW ? GRID_VIEW : LIST_VIEW;

                return setView(nextView);
            }

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

            // init
            $rootScope.aggregations = 0;

            _queryItems(undefined, Date.now())
                .catch(noop);
        },
    };
}