/* eslint-disable react/no-render-return-value */ import React from 'react'; import ReactDOM from 'react-dom'; import _ from 'lodash'; import {closeActionsMenu} from '../helpers'; import {ItemListAngularWrapper} from '../components/ItemListAngularWrapper'; import {getAndMergeRelatedEntitiesForArticles} from 'core/getRelatedEntities'; ItemList.$inject = [ '$timeout', 'search', 'monitoringState', '$rootScope', ]; /** * Handles the functionality displaying list of items from repos * (archive, ingest, publish, external, content api, archived) */ export function ItemList( $timeout, search, monitoringState, $rootScope, ) { return { link: function(scope, elem) { const abortController = new AbortController(); var groupId = scope.$id; var groups = monitoringState.state.groups || []; let firstLoad = true; monitoringState.setState({ groups: groups.concat(scope.$id), activeGroup: monitoringState.state.activeGroup || groupId, }); monitoringState.init().then(() => { var listComponent = ReactDOM.render( ( ), elem[0], ) as unknown as ItemListAngularWrapper; scope.$watch(() => monitoringState.state.activeGroup, (activeGroup) => { if (activeGroup === groupId) { listComponent.focus(); } }); /** * Test if item a equals to item b * * @param {Object} a * @param {Object} b * @return {Boolean} */ function isSameVersion(a, b) { return a._etag === b._etag && a._current_version === b._current_version && a._updated === b._updated && isSameElasticHighlights(a, b); } /** * Test if item a and item b have same elastic highlights * * @param {Object} a * @param {Object} b * @return {Boolean} */ function isSameElasticHighlights(a, b) { if (!a.es_highlight && !b.es_highlight) { return true; } if (!a.es_highlight && b.es_highlight || a.es_highlight && !b.es_highlight) { return false; } function getEsHighlight(item) { return item[0]; } return _.map(a.es_highlight, getEsHighlight).join('-') === _.map(b.es_highlight, getEsHighlight).join('-'); } /** * Test if archive_item of a equals to archive_item of b * * @param {Object} a * @param {Object} b * @return {Boolean} */ function isArchiveItemSameVersion(a, b) { if (!a.archive_item && !b.archive_item) { return true; } if (a.archive_item && b.archive_item) { if (b.archive_item.takes) { return false; // take package of the new item might have changed } return a.archive_item._current_version === b.archive_item._current_version && a.archive_item._updated === b.archive_item._updated; } return false; } /** * @ngdoc method * @name sdItemList#isContentApiItemAssociation * @private * @description * @param {object} oldItem Existing scope item * @param {object} newItem From search results * @return {boolean} returns true if the id of the featuremedia is same */ function isContentApiItemAssociation(oldItem, newItem) { if (_.get(oldItem, '_type') !== 'items' || _.get(newItem, '_type') !== 'items') { return true; } if (oldItem && newItem) { return _.get(oldItem, 'associations.featuremedia._id') === _.get(newItem, 'associations.featuremedia._id'); } return false; } scope.$watch('items', (items) => { if (!items || !items._items) { /** * The list is being reloaded. * * listComponent.loading is not updated here to avoid * loading screens being displayed too frequently. * * Due to implementation of `scheduleIfShouldUpdate` in `MonitoringGroup.ts` * reloading lists is triggered more frequently when it should be. * For example, spiking one item triggers reloading for all monitoring groups. * * Articles list code is being rewritten and the old code will be deleted * so instead of doing a proper fix I'm restoring the old behavior * which is not displaying a loading indicator at all. */ return; } var itemsList = []; var currentItems = {}; var itemsById = angular.extend({}, listComponent.state.itemsById); items._items.forEach((item) => { var itemId = search.generateTrackByIdentifier(item); var oldItem = itemsById[itemId] || null; if (!oldItem || !isSameVersion(oldItem, item) || !isArchiveItemSameVersion(oldItem, item) || !isContentApiItemAssociation(oldItem, item)) { itemsById[itemId] = angular.extend({}, oldItem, item); } if (!currentItems[itemId]) { // filter out possible duplicates currentItems[itemId] = true; itemsList.push(itemId); } }); getAndMergeRelatedEntitiesForArticles( items._items, listComponent.state.relatedEntities, abortController.signal, ).then((relatedEntities) => { listComponent.setState({ itemsList: itemsList, itemsById: itemsById, relatedEntities, view: scope.view, loading: false, }, () => { scope.rendering = scope.loading = false; }); }); }, true); scope.$watch('view', (newValue, oldValue) => { if (newValue !== oldValue) { listComponent.setState({view: newValue}); } }); scope.$watch('viewColumn', (newValue, oldValue) => { if (newValue !== oldValue) { scope.$applyAsync(() => { listComponent.setState({swimlane: newValue}); scope.refreshGroup(); }); } }); scope.$on('item:lock', (_e, data) => { var itemId = search.getTrackByIdentifier(data.item, data.item_version); listComponent.updateItem(itemId, { lock_user: data.user, lock_session: data.lock_session, lock_time: data.lock_time, _etag: data._etag, }); }); scope.$on('item:unlock', (_e, data) => { listComponent.updateAllItems(data.item, { lock_user: null, lock_session: null, lock_time: null, _etag: data._etag, }); }); scope.$on('item:unselect', () => { listComponent.setState({selected: null}); }); scope.$on('item:expired', (_e, data) => { var itemsById = angular.extend({}, listComponent.state.itemsById); var shouldUpdate = false; _.forOwn(itemsById, (item, key) => { if (data.items[item._id]) { itemsById[key] = angular.extend({gone: true}, item); shouldUpdate = true; } }); if (shouldUpdate) { listComponent.setState({itemsById: itemsById}); } }); scope.$on('item:unlink', (_e, data) => { listComponent.updateAllItems(data.item, { sequence: null, anpa_take_key: null, takes: undefined, }); }); scope.$on('item:highlights', (_e, data) => updateMarkedItems('highlights', data)); function updateMarkedItems(field, data) { var item = listComponent.findItemByPrefix(data.item_id); function filterMark(mark) { return mark !== data.mark_id; } if (item) { var itemId = search.generateTrackByIdentifier(item); var markedItems = item[field] || []; if (data.marked) { markedItems = markedItems.concat([data.mark_id]); } else { markedItems = markedItems.filter(filterMark); } listComponent.updateItem(itemId, {[field]: markedItems}); } } scope.$on('multi:reset', (e, data) => { var ids = data.ids || []; var shouldUpdate = false; var itemsById = angular.extend({}, listComponent.state.itemsById); _.forOwn(itemsById, (value, key) => { ids.forEach((id) => { if (_.startsWith(key, id)) { shouldUpdate = true; itemsById[key] = angular.extend({}, value, {selected: null}); } }); }); if (shouldUpdate) { listComponent.setState({itemsById: itemsById}); } }); scope.singleLine = search.singleLine; scope.$on('rowview:narrow', () => { listComponent.setNarrowView(true); }); scope.$on('rowview:default', () => { listComponent.setNarrowView(false); }); var updateTimeout; /** * Function for creating small delay, * before activating render function */ function handleScroll($event) { // force refresh the group or list, if scroll bar hits the top of list. if (elem[0].scrollTop === 0) { $rootScope.$broadcast('refresh:list', scope.group); } if (scope.rendering) { // ignore $event.preventDefault(); return; } // only scroll the list, not its parent $event.stopPropagation(); closeActionsMenu(); $timeout.cancel(updateTimeout); if (!scope.noScroll) { updateTimeout = $timeout(renderIfNeeded, 100, false); } } /** * Trigger render in case user scrolls to the very end of list */ function renderIfNeeded() { if (!scope.items) { return; // automatic scroll after removing items } if (isListEnd(elem[0]) && !scope.rendering) { scope.rendering = scope.loading = true; scope.fetchNext(listComponent.state.itemsList.length); } } /** * Check if we reached end of the list elem * * @param {Element} elem * @return {Boolean} */ function isListEnd(element) { return element.scrollTop + element.offsetHeight + 200 >= element.scrollHeight; } elem.on('scroll', handleScroll); // remove react elem on destroy scope.$on('$destroy', () => { abortController.abort(); elem.off(); ReactDOM.unmountComponentAtNode(elem[0]); }); scope.$on('item:actioning', (_e, data) => { listComponent.setActioning(data.item, data.actioning); }); }); }, }; }