import moment from 'moment-timezone'; import { ISuperdesk, IExtensions, IArticle, IContentProfile, IEvents, IStage, IUser, } from 'superdesk-api'; import {gettext, gettextPlural, stripHtmlTags} from 'core/utils'; import {getGenericListPageComponent} from './ui/components/ListPage/generic-list-page'; import {ListItem, ListItemColumn, ListItemActionsMenu} from './components/ListItem'; import {getFormFieldPreviewComponent} from './ui/components/generic-form/form-field'; import { isIFormGroupCollapsible, isIFormGroup, isIFormField, FormFieldType, } from './ui/components/generic-form/interfaces/form'; import {UserHtmlSingleLine} from './helpers/UserHtmlSingleLine'; import {Row, Item, Column} from './ui/components/List'; import {connectCrudManager, dataApi, dataApiByEntity} from './helpers/CrudManager'; import {elasticsearchApi} from './helpers/elasticsearch'; import {generateFilterForServer} from './ui/components/generic-form/generate-filter-for-server'; import { assertNever, Writeable, filterUndefined, filterKeys, stringToNumber, numberToString, notNullOrUndefined, } from './helpers/typescript-helpers'; import {getUrlPage, setUrlPage, urlParams} from './helpers/url'; import {downloadBlob} from './helpers/utils'; import {getLocaleForDatePicker} from './helpers/ui-framework'; import {memoize} from 'lodash'; import {Modal} from './ui/components/Modal/Modal'; import {ModalHeader} from './ui/components/Modal/ModalHeader'; import {ModalBody} from './ui/components/Modal/ModalBody'; import {ModalFooter} from './ui/components/Modal/ModalFooter'; import {SelectUser} from './ui/components/SelectUser'; import {logger} from './services/logger'; import {showModal} from './services/modalService'; import {UserAvatarFromUserId} from 'apps/users/components/UserAvatarFromUserId'; import {ArticleItemConcise} from 'core/ui/components/article-item-concise'; import {DropdownTree} from './ui/components/dropdown-tree'; import {getCssNameForExtension} from './get-css-name-for-extension'; import {Badge} from './ui/components/Badge'; import { getCustomEventNamePrefixed, getWebsocketMessageEventName, isWebsocketEventPublic, } from './notification/notification'; import {Grid} from './ui/components/grid'; import {Alert} from './ui/components/alert'; import {Figure} from './ui/components/figure'; import {DropZone} from './ui/components/drop-zone'; import {GroupLabel} from './ui/components/GroupLabel'; import {TopMenuDropdownButton} from './ui/components/TopMenuDropdownButton'; import {dispatchInternalEvent} from './internal-events'; import {Icon} from './ui/components/Icon2'; import {AuthoringWorkspaceService} from 'apps/authoring/authoring/services/AuthoringWorkspaceService'; import ng from 'core/services/ng'; import {Spacer} from './ui/components/Spacer'; import {appConfig} from 'appConfig'; import {httpRequestJsonLocal} from './helpers/network'; import {memoize as memoizeLocal} from './memoize'; import {generatePatch} from './patch'; import {getLinesCount} from 'apps/authoring/authoring/components/line-count'; import {attachmentsApi} from 'apps/authoring/attachments/attachmentsService'; import {notify} from './notify/notify'; import {sdApi} from 'api'; import {arrayToTree, treeToArray} from './helpers/tree'; import {WidgetHeading} from 'apps/dashboard/widget-heading'; function getContentType(id): Promise { return dataApi.findOne('content_types', id); } export function openArticle(id: IArticle['_id'], mode: 'view' | 'edit'): Promise { const authoringWorkspace = ng.get('authoringWorkspace'); if (document.querySelector('[sd-monitoring-view]') == null) { // redirect if outside monitoring view setUrlPage('/workspace/monitoring'); } authoringWorkspace.edit({_id: id}, mode); return Promise.resolve(); } const getContentTypeMemoized = memoize(getContentType); let getContentTypeMemoizedLastCall: number = 0; // unix time // stores a map between custom callback & callback passed to DOM // so the original event listener can be removed later const customEventMap = new Map(); export const addEventListener = (eventName: T, callback: (arg: IEvents[T]) => void) => { const handlerWrapper = (customEvent: CustomEvent) => callback(customEvent.detail); customEventMap.set(callback, handlerWrapper); window.addEventListener(getCustomEventNamePrefixed(eventName), handlerWrapper); }; export const removeEventListener = (eventName: T, callback: (arg: IEvents[T]) => void) => { const handlerWrapper = customEventMap.get(callback); if (handlerWrapper != null) { window.removeEventListener(getCustomEventNamePrefixed(eventName), handlerWrapper); customEventMap.delete(callback); } }; let applicationState: Writeable = { articleInEditMode: undefined, }; addEventListener('articleEditStart', (article) => { applicationState.articleInEditMode = article._id; }); addEventListener('articleEditEnd', () => { delete applicationState['articleInEditMode']; }); export function isLockedInCurrentSession(article: IArticle): boolean { return ng.get('lock').isLockedInCurrentSession(article); } export function isLockedInOtherSession(article: IArticle): boolean { return sdApi.article.isLocked(article) && !isLockedInCurrentSession(article); } export const formatDate = (date: Date | string) => ( moment(date) .tz(appConfig.defaultTimezone) .format(appConfig.view.dateformat) ); export function getRelativeOrAbsoluteDateTime( datetimeString: string, format: string, relativeDuration: number = 1, relativeUnit: string = 'days', ): string { const datetime = moment(datetimeString); if (datetime.isSameOrAfter(moment().subtract(relativeDuration, relativeUnit))) { return datetime.fromNow(); } return datetime .tz(appConfig.defaultTimezone) .format(format); } // imported from planning export function getSuperdeskApiImplementation( requestingExtensionId: string, extensions: IExtensions, modal, privileges, lock, session, authoringWorkspace: AuthoringWorkspaceService, config, metadata, preferencesService, ): ISuperdesk { return { dataApi: dataApi, dataApiByEntity, elasticsearch: elasticsearchApi, helpers: { assertNever, filterUndefined, filterKeys, stringToNumber, numberToString, notNullOrUndefined, }, httpRequestJsonLocal, entities: { article: { isPersonal: sdApi.article.isPersonal, isLocked: sdApi.article.isLocked, isLockedInCurrentSession: sdApi.article.isLockedInCurrentSession, isLockedInOtherSession: sdApi.article.isLockedInOtherSession, patch: (article, patch, dangerousOptions) => { const onPatchBeforeMiddlewares = Object.values(extensions) .map((extension) => extension.activationResult?.contributions?.entities?.article?.onPatchBefore) .filter((middleware) => middleware != null); return onPatchBeforeMiddlewares.reduce( (current, next) => current.then((result) => next(article._id, result, dangerousOptions)), Promise.resolve(patch), ).then((patchFinal) => { return dataApi.patchRaw( // distinction between handling published and non-published items // should be removed: SDESK-4687 (sdApi.article.isPublished(article) ? 'published' : 'archive'), article._id, article._etag, patchFinal, ).then((res) => { if (dangerousOptions?.patchDirectlyAndOverwriteAuthoringValues === true) { dispatchInternalEvent( 'dangerouslyOverwriteAuthoringData', {...patch, _etag: res._etag, _id: res._id}, ); } }); }).catch((err) => { if (err instanceof Error) { logger.error(err); } }); }, isArchived: sdApi.article.isArchived, isPublished: (article) => sdApi.article.isPublished(article), }, desk: { getStagesOrdered: (deskId: string) => dataApi.query('stages', 1, {field: '_id', direction: 'ascending'}, {desk: deskId}, 200) .then((response) => response._items), }, contentProfile: { get: (id) => { // Adding simple caching since the function will be called multiple times per second. // TODO: implement synchronous API(and a cache) for accessing // most user settings including content profiles. const timestamp = Date.now(); // cache for 5 seconds if (timestamp - getContentTypeMemoizedLastCall > 5000) { getContentTypeMemoized.cache.clear(); } getContentTypeMemoizedLastCall = timestamp; return getContentTypeMemoized(id); }, }, vocabulary: { getIptcSubjects: () => metadata.initialize().then(() => metadata.values.subjectcodes), getVocabulary: (id: string) => metadata.initialize().then(() => metadata.values[id]), }, attachment: attachmentsApi, users: { getUsersByIds: (ids) => ( dataApi.query( 'users', 1, {field: 'display_name', direction: 'ascending'}, {_id: {$in: ids}}, 200, ) .then((response) => response._items) ), }, }, state: applicationState, instance: { config, }, ui: { article: { view: (id: IArticle['_id']) => { openArticle(id, 'view'); }, addImage: (field: string, image: IArticle) => { dispatchInternalEvent('addImage', {field, image}); }, save: () => { dispatchInternalEvent('saveArticleInEditMode', null); }, }, alert: (message: string) => modal.alert({bodyText: message}), confirm: (message: string, title?: string) => new Promise((resolve) => { modal.confirm(message, title ?? gettext('Cancel')) .then(() => resolve(true)) .catch(() => resolve(false)); }), showModal, notify: notify, framework: { getLocaleForDatePicker, }, }, components: { UserHtmlSingleLine, getGenericListPageComponent, connectCrudManager, ListItem, ListItemColumn, ListItemActionsMenu, List: { // there's no full React implementation of ListItem component // https://superdesk.github.io/superdesk-ui-framework/dist/#/list-item // as operator is used in order to prevent exposing more props // so it's easier to remove old usages when we have a full implementation Item: Item as React.ComponentType<{onClick: any}>, Row: Row as React.ComponentType, Column: Column as React.ComponentType<{grow: boolean}>, }, Grid, Alert, Figure, DropZone, Modal, ModalHeader, ModalBody, ModalFooter, Badge, SelectUser, UserAvatar: UserAvatarFromUserId, ArticleItemConcise, GroupLabel, TopMenuDropdownButton, Icon, getDropdownTree: () => DropdownTree, Spacer, WidgetHeading, }, forms: { FormFieldType, generateFilterForServer, isIFormGroupCollapsible, isIFormGroup, isIFormField, getFormFieldPreviewComponent, }, localization: { gettext: (message, params) => gettext(message, params), gettextPlural: (count, singular, plural, params) => gettextPlural(count, singular, plural, params), formatDate: formatDate, formatDateTime: (date: Date) => { return moment(date) .tz(appConfig.defaultTimezone) .format(appConfig.view.dateformat + ' ' + appConfig.view.timeformat); }, longFormatDateTime: (date: Date | string) => { return moment(date) .tz(appConfig.defaultTimezone) .format(appConfig.longDateFormat || 'LLL'); }, getRelativeOrAbsoluteDateTime: getRelativeOrAbsoluteDateTime, }, privileges: { getOwnPrivileges: () => privileges.loaded.then(() => privileges.privileges), hasPrivilege: (privilege: string) => privileges.userHasPrivileges({[privilege]: 1}), }, preferences: { get: (key) => { return preferencesService.get().then((res: Dictionary) => { return res?.extensions?.[requestingExtensionId]?.[key] ?? null; }); }, set: (key, value) => { return preferencesService.get().then((res: Dictionary) => { const extensionsPreferences = res.extensions ?? {}; if (extensionsPreferences[requestingExtensionId] == null) { extensionsPreferences[requestingExtensionId] = {}; } extensionsPreferences[requestingExtensionId][key] = value; return preferencesService.update({extensions: extensionsPreferences}); }); }, }, session: { getToken: () => session.token, getCurrentUser: () => session.getIdentity(), getSessionId: () => session.sessionId, getCurrentUserId: () => session.identity._id, }, browser: { location: { getPage: getUrlPage, setPage: setUrlPage, urlParams: urlParams, }, }, utilities: { logger, CSS: { getClass: (originalName: string) => getCssNameForExtension(originalName, requestingExtensionId), getId: (originalName: string) => getCssNameForExtension(originalName, requestingExtensionId), }, dateToServerString: (date: Date) => { return date.toISOString().slice(0, 19) + '+0000'; }, memoize: memoizeLocal, generatePatch, stripHtmlTags, getLinesCount, downloadBlob, arrayToTree, treeToArray, }, addWebsocketMessageListener: (eventName, handler) => { const eventNameFinal = getWebsocketMessageEventName( eventName, isWebsocketEventPublic(eventName) ? undefined : requestingExtensionId, ); window.addEventListener(eventNameFinal, handler); return () => { window.removeEventListener(eventNameFinal, handler); }; }, addEventListener, removeEventListener, }; }