scripts/apps/monitoring/directives/WidgetGroup.ts
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]);
});
},
};
}