From bcfe756c1e39ae7e905a29b8cafed9af21d8dd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ja=C5=A1ek?= Date: Tue, 13 Aug 2024 16:40:17 +0200 Subject: [PATCH 1/8] fix protractor e2e (#4597) * fix protractor e2e with recent webdriver * fix more tests --- e2e/client/specs/authoring_spec.ts | 24 +++++++++--------------- e2e/client/specs/content_spec.ts | 4 ++-- e2e/client/specs/editor3_spec.ts | 2 +- e2e/client/specs/helpers/authoring.ts | 8 +++++--- e2e/client/specs/helpers/monitoring.ts | 22 ++++++++++++++++++++-- e2e/client/specs/helpers/search.ts | 2 ++ e2e/client/specs/monitoring_spec.ts | 3 +-- 7 files changed, 40 insertions(+), 25 deletions(-) diff --git a/e2e/client/specs/authoring_spec.ts b/e2e/client/specs/authoring_spec.ts index fb71848dd6..d5dbc63cf9 100644 --- a/e2e/client/specs/authoring_spec.ts +++ b/e2e/client/specs/authoring_spec.ts @@ -58,8 +58,7 @@ describe('authoring', () => { it('authoring operations', () => { // allows to create a new empty package - el(['content-create']).click(); - el(['content-create-dropdown', 'create-package']).click(); + monitoring.createItem('Create package'); expect(element(by.className('packaging-screen')).isDisplayed()).toBe(true); authoring.close(); @@ -615,8 +614,8 @@ describe('authoring', () => { it('Not modifying crops will not trigger an article change', () => { workspace.selectDesk('XEditor3 Desk'); // has media gallery in content profile - el(['content-create']).click(); - el(['content-create-dropdown']).element(by.buttonText('editor3 template')).click(); + monitoring.createItem('editor3 template'); + browser.wait(ECE.visibilityOf( element(by.css(s('authoring-field=Image gallery 33', 'media-gallery--upload-placeholder'))), )); @@ -645,15 +644,16 @@ describe('authoring', () => { it('Can add an image with default crops to media gallery', () => { workspace.selectDesk('XEditor3 Desk'); // has media gallery in content profile - el(['content-create']).click(); - el(['content-create-dropdown']).element(by.buttonText('editor3 template')).click(); + monitoring.createItem('editor3 template'); + browser.wait(ECE.visibilityOf( element(by.css(s('authoring-field=Image gallery 33', 'media-gallery--upload-placeholder'))), )); - expect(ECE.hasElementCount( + + browser.wait(ECE.hasElementCount( element.all(by.css(s('authoring-field=Image gallery 33', 'media-gallery-image'))), 0, - )()).toBe(true); + )); uploadMedia(getAbsoluteFilePath('test-files/image-big.jpg')); @@ -666,13 +666,7 @@ describe('authoring', () => { it('Can remove an image from media gallery', () => { workspace.selectDesk('XEditor3 Desk'); // has media gallery in content profile - el(['content-create']).click(); - - const templateBtn = el(['content-create-dropdown']).element(by.buttonText('editor3 template')); - - browser.wait(ECE.elementToBeClickable(templateBtn)); - - templateBtn.click(); + monitoring.createItem('editor3 template'); browser.wait(ECE.visibilityOf( element(by.css(s('authoring-field=Image gallery 33', 'media-gallery--upload-placeholder'))), diff --git a/e2e/client/specs/content_spec.ts b/e2e/client/specs/content_spec.ts index b495725a9d..69ce29f5cc 100644 --- a/e2e/client/specs/content_spec.ts +++ b/e2e/client/specs/content_spec.ts @@ -5,6 +5,7 @@ import {element, browser, protractor, by} from 'protractor'; import {workspace} from './helpers/workspace'; import {content} from './helpers/content'; import {authoring} from './helpers/authoring'; +import {monitoring} from './helpers/monitoring'; import {multiAction} from './helpers/actions'; import {ECE, el} from '@superdesk/end-to-end-testing-helpers'; import {TreeSelectDriver} from './helpers/tree-select-driver'; @@ -164,8 +165,7 @@ describe('content', () => { workspace.switchToDesk('SPORTS DESK'); content.setListView(); - el(['content-create']).click(); - el(['content-create-dropdown', 'create-package']).click(); + monitoring.createItem('Create package'); element.all(by.model('item.headline')).first().sendKeys('Empty Package'); authoring.save(); diff --git a/e2e/client/specs/editor3_spec.ts b/e2e/client/specs/editor3_spec.ts index 57607e58ce..8a5f21fbcd 100644 --- a/e2e/client/specs/editor3_spec.ts +++ b/e2e/client/specs/editor3_spec.ts @@ -62,7 +62,7 @@ describe('editor3', () => { monitoring.openMonitoring(); monitoring.selectDesk('xeditor3'); monitoring.createFromDeskTemplate(); - browser.wait(ECE.presenceOf(editors.get(0))); + browser.wait(ECE.presenceOf(editors.get(0)), 2000); }); it('can edit headline', () => { diff --git a/e2e/client/specs/helpers/authoring.ts b/e2e/client/specs/helpers/authoring.ts index 9a6e88c43c..c88265af3f 100644 --- a/e2e/client/specs/helpers/authoring.ts +++ b/e2e/client/specs/helpers/authoring.ts @@ -5,6 +5,7 @@ import {waitHidden, waitFor, click} from './utils'; import {ECE, els, el} from '@superdesk/end-to-end-testing-helpers'; import {PLAIN_TEXT_TEMPLATE_NAME} from './constants'; import {TreeSelectDriver} from './tree-select-driver'; +import {monitoring} from './monitoring'; class Authoring { lock: any; @@ -337,11 +338,12 @@ class Authoring { * @param {String} name */ this.createTextItemFromTemplate = (name) => { - el(['content-create']).click(); - el(['content-create-dropdown'], by.buttonText('More templates...')).click(); + monitoring.createItem('More templates...'); + el(['content-create-dropdown', 'search']).sendKeys(name); el(['content-create-dropdown'], by.buttonText(name)).click(); - browser.wait(ECE.presenceOf(el(['authoring']))); + + browser.wait(ECE.presenceOf(el(['authoring'])), 2000); }; this.close = function() { diff --git a/e2e/client/specs/helpers/monitoring.ts b/e2e/client/specs/helpers/monitoring.ts index 80831bebf0..60141cfc14 100644 --- a/e2e/client/specs/helpers/monitoring.ts +++ b/e2e/client/specs/helpers/monitoring.ts @@ -25,6 +25,7 @@ class Monitoring { showSpiked: () => void; showPersonal: () => void; showSearch: () => void; + createItem: (template: string) => void; createFromDeskTemplate: () => any; getGroup: (group: number) => any; getGroups: () => any; @@ -171,8 +172,14 @@ class Monitoring { * Create new item using desk template */ this.createFromDeskTemplate = () => { - el(['content-create']).click(); - el(['content-create-dropdown', 'default-desk-template']).click(); + const createButton = el(['content-create']); + const templateButton = el(['content-create-dropdown', 'default-desk-template']); + + browser.wait(ECE.elementToBeClickable(createButton), 1000); + createButton.click(); + + browser.wait(ECE.elementToBeClickable(templateButton), 1000); + templateButton.click(); }; this.getGroup = function(group: number) { @@ -992,6 +999,17 @@ class Monitoring { this.getPackageItemLabel = function(index) { return element.all(by.id('package-item-label')).get(index); }; + + this.createItem = (buttonText: string) => { + const plusButton = el(['content-create']); + const itemButton = el(['content-create-dropdown']).element(by.buttonText(buttonText)); + + browser.wait(ECE.elementToBeClickable(plusButton), 2000); + plusButton.click(); + + browser.wait(ECE.elementToBeClickable(itemButton), 2000, `Button '${buttonText}' is not clickable`); + itemButton.click(); + }; } } diff --git a/e2e/client/specs/helpers/search.ts b/e2e/client/specs/helpers/search.ts index d082bed92e..eba41fc20e 100644 --- a/e2e/client/specs/helpers/search.ts +++ b/e2e/client/specs/helpers/search.ts @@ -147,6 +147,8 @@ class GlobalSearch { this.itemClick = function(index) { var itemElem = this.getItem(index); + browser.wait(ECE.elementToBeClickable(itemElem), 1000); + itemElem.click(); browser.wait(ECE.attributeContains(itemElem, 'class', 'active'), 2000); diff --git a/e2e/client/specs/monitoring_spec.ts b/e2e/client/specs/monitoring_spec.ts index cea1f00538..642a39a714 100644 --- a/e2e/client/specs/monitoring_spec.ts +++ b/e2e/client/specs/monitoring_spec.ts @@ -473,8 +473,7 @@ describe('monitoring', () => { it('can start content upload', () => { monitoring.openMonitoring(); - el(['content-create']).click(); - el(['content-create-dropdown', 'upload-media']).click(); + monitoring.createItem('Upload media'); expect(monitoring.uploadModal.isDisplayed()).toBeTruthy(); }); From fffbc6caa8439b862f4bde5c6d4a96e75acc89af Mon Sep 17 00:00:00 2001 From: Konstantin Markov Date: Wed, 14 Aug 2024 14:41:20 +0300 Subject: [PATCH 2/8] =?UTF-8?q?Refactor=20all=20notifications=20from=20ext?= =?UTF-8?q?ensions=20and=20UI=20toggling=20such=20that=20=E2=80=A6=20(#458?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/apps/search/index.ts | 2 +- .../EmailNotificationPreferences.tsx | 34 ++++--- .../directives/UserPreferencesDirective.ts | 90 ++++++++++--------- .../apps/users/views/user-preferences.html | 8 +- .../core/menu/notifications/notifications.ts | 10 +-- scripts/core/services/preferencesService.ts | 3 +- scripts/core/superdesk-api.d.ts | 29 +++--- .../broadcasting/src/notifications.ts | 3 +- .../extensions/markForUser/src/extension.tsx | 17 +--- .../src/get-marked-for-me-component.tsx | 4 - 10 files changed, 98 insertions(+), 102 deletions(-) diff --git a/scripts/apps/search/index.ts b/scripts/apps/search/index.ts index cadf2d69f0..03c14b6e59 100644 --- a/scripts/apps/search/index.ts +++ b/scripts/apps/search/index.ts @@ -78,7 +78,7 @@ angular.module('superdesk.apps.search', [ 'sdEmailNotificationsList', reactToAngular1( EmailNotificationPreferences, - ['toggleEmailNotification', 'preferences'], + ['toggleEmailNotification', 'preferences', 'notificationLabels'], ), ) diff --git a/scripts/apps/users/components/EmailNotificationPreferences.tsx b/scripts/apps/users/components/EmailNotificationPreferences.tsx index 4ca1cca2ab..aeba5fbe27 100644 --- a/scripts/apps/users/components/EmailNotificationPreferences.tsx +++ b/scripts/apps/users/components/EmailNotificationPreferences.tsx @@ -1,25 +1,37 @@ import React from 'react'; import {CheckGroup, Checkbox} from 'superdesk-ui-framework/react'; +import {IUser} from 'superdesk-api'; +import {gettext} from 'core/utils'; interface IProps { toggleEmailNotification: (notificationId: string) => void; - preferences?: {[key: string]: any}; + preferences: { + notifications: IUser['user_preferences']['notifications']; + }; + notificationLabels: Dictionary; } export class EmailNotificationPreferences extends React.PureComponent { render(): React.ReactNode { return ( - {Object.entries(this.props.preferences ?? []).map(([key, value]) => ( - { - this.props.toggleEmailNotification(key); - }} - checked={value?.enabled ?? value?.default ?? false} - /> - ))} + {Object.entries(this.props.preferences.notifications) + .map(([notificationId, notificationSettings]) => { + return ( + { + this.props.toggleEmailNotification(notificationId); + }} + checked={notificationSettings.email} + /> + ); + })} ); } diff --git a/scripts/apps/users/directives/UserPreferencesDirective.ts b/scripts/apps/users/directives/UserPreferencesDirective.ts index bf3c213f3d..7627cac87f 100644 --- a/scripts/apps/users/directives/UserPreferencesDirective.ts +++ b/scripts/apps/users/directives/UserPreferencesDirective.ts @@ -4,6 +4,8 @@ import {gettext} from 'core/utils'; import {appConfig, extensions, getUserInterfaceLanguage} from 'appConfig'; import {applyDefault} from 'core/helpers/typescript-helpers'; import {DEFAULT_EDITOR_THEME} from 'apps/authoring/authoring/services/AuthoringThemesService'; +import {cloneDeep, pick} from 'lodash'; +import {IExtensionActivationResult} from 'superdesk-api'; /** * @ngdoc directive @@ -43,65 +45,48 @@ export function UserPreferencesDirective( link: function(scope, element, attrs) { const userLang = getUserInterfaceLanguage().replace('_', '-'); const body = angular.element('body'); + const NOTIFICATIONS_KEY = 'notifications'; scope.activeNavigation = null; - scope.activeTheme = localStorage.getItem('theme'); + const registeredNotifications: IExtensionActivationResult['contributions']['notifications'] = (() => { + const result = {}; + + for (const extension of Object.values(extensions)) { + for (const [notificationId, notification] of Object.entries(extension.activationResult.contributions?.notifications ?? [])) { + result[notificationId] = notification; + } + } + + return result; + })(); /* * Set this to true after adding all the preferences to the scope. If done before, then the * directives which depend on scope variables might fail to load properly. */ - scope.preferencesLoaded = false; var orig: {[key: string]: any}; // original preferences, before any changes - scope.emailNotificationsFromExtensions = {}; - - scope.buildNotificationsFromExtensions = function() { - for (const extension of Object.values(extensions)) { - for (const [key, value] of Object.entries(extension.activationResult.contributions?.notifications ?? [])) { - if (value.type === 'email') { - preferencesService.registerUserPreference(key, 1); - scope.emailNotificationsFromExtensions[key] = preferencesService.getSync(key); - } - } - } - }; - - scope.buildNotificationsFromExtensions(); - + // email:notification toggling happens via `ng-model` in a template + // this function only updates child notifications scope.toggleEmailGroupNotifications = function() { const isGroupEnabled = scope.preferences['email:notification'].enabled; - Object.keys(scope.emailNotificationsFromExtensions).forEach((notificationId) => { - scope.preferences[notificationId].enabled = isGroupEnabled; - scope.emailNotificationsFromExtensions[notificationId] = { - ...scope.emailNotificationsFromExtensions[notificationId], - enabled: isGroupEnabled, - }; - }); + for (const notificationId of Object.keys(scope.preferences.notifications)) { + scope.preferences[NOTIFICATIONS_KEY][notificationId].email = isGroupEnabled; + } scope.userPrefs.$setDirty(); scope.$applyAsync(); }; scope.toggleEmailNotification = function(notificationId: string) { - const enabledUpdate = !(scope.preferences[notificationId]?.enabled ?? false); + scope.preferences[NOTIFICATIONS_KEY][notificationId].email = + !scope.preferences[NOTIFICATIONS_KEY][notificationId].email; - scope.preferences[notificationId] = { - ...(scope.preferences[notificationId] ?? {}), - enabled: enabledUpdate, - }; - scope.emailNotificationsFromExtensions[notificationId] = { - ...scope.emailNotificationsFromExtensions[notificationId], - enabled: enabledUpdate, - }; - - const notificationsForGroupAreOff = Object.values(scope.emailNotificationsFromExtensions) - .every((value: any) => value?.enabled == false); - - scope.preferences['email:notification'].enabled = !notificationsForGroupAreOff; + scope.preferences['email:notification'].enabled = + Object.values(scope.preferences[NOTIFICATIONS_KEY]).some((value: any) => value.email === true); scope.userPrefs.$setDirty(); scope.$applyAsync(); @@ -109,7 +94,7 @@ export function UserPreferencesDirective( preferencesService.get(null, true).then((result) => { orig = result; - buildPreferences(orig); + buildPreferences(cloneDeep(result)); scope.datelineSource = session.identity.dateline_source; scope.datelinePreview = scope.preferences['dateline:located'].located; @@ -118,7 +103,7 @@ export function UserPreferencesDirective( scope.cancel = function() { scope.userPrefs.$setPristine(); - buildPreferences(orig); + buildPreferences(cloneDeep(orig)); scope.datelinePreview = scope.preferences['dateline:located'].located; }; @@ -159,7 +144,7 @@ export function UserPreferencesDirective( }); }, () => $q.reject('canceledByModal')) .then((preferences) => { - // ask for browser permission if desktop notification is enable + // ask for browser permission if desktop notification is enable if (_.get(preferences, 'desktop:notification.enabled')) { preferencesService.desktopNotification.requestPermission(); } @@ -289,13 +274,13 @@ export function UserPreferencesDirective( scope.preferences = {}; _.each(data, (val, key) => { - if (val.label && val.category) { + if (key == NOTIFICATIONS_KEY) { + scope.preferences[NOTIFICATIONS_KEY] = pick(val, Object.keys(registeredNotifications)); + } else if (val.label && val.category) { scope.preferences[key] = _.create(val); } }); - scope.buildNotificationsFromExtensions(); - // metadata service initialization is needed if its // values object is undefined or any of the needed // data buckets are missing in it @@ -400,6 +385,23 @@ export function UserPreferencesDirective( scope.calendars = helperData.event_calendars; + scope.notificationLabels = {}; + + if (scope.preferences[NOTIFICATIONS_KEY] == null) { + scope.preferences[NOTIFICATIONS_KEY] = {}; + } + + for (const [notificationId, notification] of Object.entries(registeredNotifications)) { + if (scope.preferences[NOTIFICATIONS_KEY][notificationId] == null) { + scope.preferences[NOTIFICATIONS_KEY][notificationId] = { + email: true, + desktop: true, + }; + } + + scope.notificationLabels[notificationId] = notification.name; + } + scope.preferencesLoaded = true; } diff --git a/scripts/apps/users/views/user-preferences.html b/scripts/apps/users/views/user-preferences.html index b694ce4ca8..6ec1f938e0 100644 --- a/scripts/apps/users/views/user-preferences.html +++ b/scripts/apps/users/views/user-preferences.html @@ -66,7 +66,7 @@

+
  • Notifications

    @@ -82,10 +82,10 @@

    - + data-notification-labels="notificationLabels" + >

    diff --git a/scripts/core/menu/notifications/notifications.ts b/scripts/core/menu/notifications/notifications.ts index ca86008d43..bda1ced344 100644 --- a/scripts/core/menu/notifications/notifications.ts +++ b/scripts/core/menu/notifications/notifications.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; import {gettext} from 'core/utils'; import {AuthoringWorkspaceService} from 'apps/authoring/authoring/services/AuthoringWorkspaceService'; import {extensions} from 'appConfig'; -import {IDesktopNotification} from 'superdesk-api'; +import {IExtensionActivationResult} from 'superdesk-api'; import {logger} from 'core/services/logger'; import emptyState from 'superdesk-ui-framework/dist/empty-state--small-2.svg'; @@ -296,7 +296,9 @@ angular.module('superdesk.core.menu.notifications', ['superdesk.core.services.as scope.emptyState = emptyState; // merged from all extensions - const notificationsKeyed: {[key: string]: IDesktopNotification['handler']} = {}; + const notificationsKeyed: { + [key: string]: IExtensionActivationResult['contributions']['notifications'][0]['handler'] + } = {}; for (const extension of Object.values(extensions)) { const notificationsFromExtensions = extension.activationResult.contributions?.notifications; @@ -306,9 +308,7 @@ angular.module('superdesk.core.menu.notifications', ['superdesk.core.services.as if (notificationsKeyed[key] == null) { const notificationValue = notificationsFromExtensions[key]; - if (notificationValue.type == 'desktop') { - notificationsKeyed[key] = notificationValue.handler; - } + notificationsKeyed[key] = notificationValue.handler; } else { logger.error(new Error(`Notification key ${key} already registered.`)); } diff --git a/scripts/core/services/preferencesService.ts b/scripts/core/services/preferencesService.ts index ed169e192d..6b7c64f987 100644 --- a/scripts/core/services/preferencesService.ts +++ b/scripts/core/services/preferencesService.ts @@ -23,6 +23,7 @@ export default angular.module('superdesk.core.preferences', ['superdesk.core.not userPreferences = { 'feature:preview': 1, 'archive:view': 1, + 'notifications': 1, 'email:notification': 1, 'desktop:notification': 1, 'slack:notification': 1, @@ -90,7 +91,7 @@ export default angular.module('superdesk.core.preferences', ['superdesk.core.not }, // ask for permission and send a desktop notification send: (msg) => { - if (_.get(preferences, 'user_preferences.desktop:notification.enabled')) { + if (preferences.user_preferences['desktop:notification'].enabled) { if ('Notification' in window && Notification.permission !== 'denied') { Notification.requestPermission((permission) => { if (permission === 'granted') { diff --git a/scripts/core/superdesk-api.d.ts b/scripts/core/superdesk-api.d.ts index 8d9599f3bb..1e1c3e7604 100644 --- a/scripts/core/superdesk-api.d.ts +++ b/scripts/core/superdesk-api.d.ts @@ -705,19 +705,6 @@ declare module 'superdesk-api' { preview?: React.ComponentType; } - interface IEmailNotification { - type: 'email'; - } - - export interface IDesktopNotification { - type: 'desktop'; - label: string; - handler: (notification: any) => { - body: string; - actions: Array<{label: string; onClick: () => void;}>; - }; - } - export interface IExtensionActivationResult { contributions?: { globalMenuHorizontal?: Array; @@ -756,7 +743,13 @@ declare module 'superdesk-api' { workspaceMenuItems?: Array; customFieldTypes?: Array; notifications?: { - [id: string]: IEmailNotification | IDesktopNotification; + [id: string]: { + name: string; + handler?: (notification: any) => { + body: string; + actions: Array<{label: string; onClick: () => void;}>; + }; + }; }; entities?: { article?: { @@ -1421,6 +1414,14 @@ declare module 'superdesk-api' { invisible_stages: Array; slack_username: string; slack_user_id: string; + user_preferences: { + notifications: { + [key: string]: { + email: boolean; + desktop: boolean; + }; + }; + }; last_activity_at?: string; } diff --git a/scripts/extensions/broadcasting/src/notifications.ts b/scripts/extensions/broadcasting/src/notifications.ts index e9534c0887..44dfcc90a1 100644 --- a/scripts/extensions/broadcasting/src/notifications.ts +++ b/scripts/extensions/broadcasting/src/notifications.ts @@ -19,8 +19,7 @@ type IExtensionNotifications = Required['co export const notifications: IExtensionNotifications = { 'rundown-item-comment': { - label: gettext('Open item'), - type: 'desktop', + name: gettext('Open item'), handler: (notification: IRundownItemCommentNotification) => ({ body: notification.message, actions: [{ diff --git a/scripts/extensions/markForUser/src/extension.tsx b/scripts/extensions/markForUser/src/extension.tsx index a60507836d..8332eaba63 100644 --- a/scripts/extensions/markForUser/src/extension.tsx +++ b/scripts/extensions/markForUser/src/extension.tsx @@ -48,8 +48,7 @@ const extension: IExtension = { }, notifications: { 'item:marked': { - type: 'desktop', - label: gettext('open item'), + name: gettext('Mark for User'), handler: (notification: any) => ({ body: notification.message, actions: [{ @@ -58,20 +57,6 @@ const extension: IExtension = { }], }), }, - 'item:unmarked': { - label: gettext('open item'), - type: 'desktop', - handler: (notification: any) => ({ - body: notification.message, - actions: [{ - label: gettext('open item'), - onClick: () => superdesk.ui.article.view(notification.item), - }], - }), - }, - 'mark_for_user:notification': { - type: 'email', - }, }, entities: { article: { diff --git a/scripts/extensions/markForUser/src/get-marked-for-me-component.tsx b/scripts/extensions/markForUser/src/get-marked-for-me-component.tsx index b55d665208..3d8accc420 100644 --- a/scripts/extensions/markForUser/src/get-marked-for-me-component.tsx +++ b/scripts/extensions/markForUser/src/get-marked-for-me-component.tsx @@ -72,10 +72,6 @@ export function getMarkedForMeComponent(superdesk: ISuperdesk) { }); this.removeMarkedListener = superdesk.addWebsocketMessageListener('item:marked', this.queryAndSetArticles); - this.removeUnmarkedListener = superdesk.addWebsocketMessageListener( - 'item:unmarked', - this.queryAndSetArticles, - ); } componentWillUnmount() { this.removeMarkedListener(); From 43fe3e7a041f2d95b49371b8632bba14d59ba75f Mon Sep 17 00:00:00 2001 From: Konstantin Markov Date: Wed, 14 Aug 2024 15:45:43 +0300 Subject: [PATCH 3/8] Port embed test to playwright (#4440) --- e2e/client/playwright/editor3.spec.ts | 41 +++++++++++++++++++ .../editor3/components/embeds/EmbedInput.tsx | 21 +++++++--- .../editor3/components/tests/embeds.spec.tsx | 22 ---------- .../editor3/components/toolbar/IconButton.tsx | 2 +- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/e2e/client/playwright/editor3.spec.ts b/e2e/client/playwright/editor3.spec.ts index 9ecf26ff16..cc1d5a426d 100644 --- a/e2e/client/playwright/editor3.spec.ts +++ b/e2e/client/playwright/editor3.spec.ts @@ -4,6 +4,47 @@ import {restoreDatabaseSnapshot, s} from './utils'; import {getEditor3FormattingOptions, getEditor3Paragraphs} from './utils/editor3'; import {TreeSelectDriver} from './utils/tree-select-driver'; +test('can add embeds', async ({page}) => { + await restoreDatabaseSnapshot(); + + const monitoring = new Monitoring(page); + + const requestRoute = 'https://sourcefabric.org'; + + await page.route( + `https://iframe.ly/api/oembed?callback=?&url= + ${requestRoute} + &api_key="mock_api_key" + &omit_script=true&iframe=true`, + (route) => { + route.fulfill({ + body: JSON.stringify([{ + title: 'Open Source Software for Journalism', + description: 'Sourcefabric is Europe\'s largest developer of ' + + 'open source tools for news media, powering news and media organisations around the world.', + }]), + }); + }, + ); + await page.goto('/#/workspace/monitoring'); + + await monitoring.selectDeskOrWorkspace('Sports'); + + await page.locator( + s('monitoring-group=Sports / Working Stage', 'article-item=test sports story'), + ).dblclick(); + + page.locator(s('toolbar')).getByRole('button', {name: 'Embed'}).click(); + + await page.locator(s('embed-form')).getByPlaceholder('Enter URL or code to embed') + .fill('https://sourcefabric.org'); + + await page.locator(s('embed-controls', 'submit')).click(); + await expect( + page.locator(s('authoring', 'authoring-field=body_html')).getByText('https://sourcefabric.org'), + ).toBeDefined(); +}); + test('accepting a spelling suggestion', async ({page}) => { const monitoring = new Monitoring(page); diff --git a/scripts/core/editor3/components/embeds/EmbedInput.tsx b/scripts/core/editor3/components/embeds/EmbedInput.tsx index 76ea0ad739..889d4d45b2 100644 --- a/scripts/core/editor3/components/embeds/EmbedInput.tsx +++ b/scripts/core/editor3/components/embeds/EmbedInput.tsx @@ -113,8 +113,12 @@ export class EmbedInputComponent extends React.Component { } getEmbedObject(value) - .then((data) => data.type === 'link' ? $.Deferred().reject() : data) - .then(this.processSuccess, this.processError); + .then((data) => { + this.processSuccess(data); + }) + .catch((error) => { + this.processError(error); + }); } /** @@ -136,7 +140,12 @@ export class EmbedInputComponent extends React.Component { const {error} = this.state; return ( -
    + @@ -154,11 +163,11 @@ export class EmbedInputComponent extends React.Component { }} placeholder={gettext('Enter URL or code to embed')} /> -
    - + diff --git a/scripts/core/editor3/components/tests/embeds.spec.tsx b/scripts/core/editor3/components/tests/embeds.spec.tsx index 727887f385..d771f12cfe 100644 --- a/scripts/core/editor3/components/tests/embeds.spec.tsx +++ b/scripts/core/editor3/components/tests/embeds.spec.tsx @@ -92,26 +92,4 @@ describe('editor3.components.embed-input', () => { expect(wrapper.state('error')).toBe('this is the error'); expect(wrapper.find('.embed-dialog__error').text()).toBe('this is the error'); })); - - it('should call onSubmit and reset error on success', inject(($q, $rootScope) => { - const {options} = mockStore(); - const onCancel = jasmine.createSpy(); - const onSubmit = jasmine.createSpy(); - const wrapper = mount(, options); - - spyOn($, 'ajax').and.returnValue($q.resolve({html: 'foo'})); - - wrapper.setState({error: 'some error'}); - - const instance: any = wrapper.find('input').instance(); - - instance.value = 'http://will.fail'; - wrapper.simulate('submit'); - - $rootScope.$apply(); - - expect(onSubmit).toHaveBeenCalledWith({html: 'foo'}); - expect(onCancel).toHaveBeenCalled(); - expect(wrapper.state('error')).toBe(''); - })); }); diff --git a/scripts/core/editor3/components/toolbar/IconButton.tsx b/scripts/core/editor3/components/toolbar/IconButton.tsx index 105eaeedc9..5134f2b9e1 100644 --- a/scripts/core/editor3/components/toolbar/IconButton.tsx +++ b/scripts/core/editor3/components/toolbar/IconButton.tsx @@ -26,6 +26,6 @@ export const IconButton: React.FunctionComponent = ({onClick, iconName, style={uiTheme == null ? undefined : {color: uiTheme.textColor}} role="button" > - +
    ); From df0566ea9c320966c0f89560db8d46b72a49ca95 Mon Sep 17 00:00:00 2001 From: Tomas Kikutis Date: Wed, 14 Aug 2024 16:07:34 +0200 Subject: [PATCH 4/8] export prepareSuperdeskQuery (#4600) --- e2e/client/playwright/multiedit.spec.ts | 21 ++++++++++++------- e2e/client/playwright/utils/inputs.tsx | 10 +++++++++ .../core/get-superdesk-api-implementation.tsx | 4 +++- scripts/core/superdesk-api.d.ts | 10 +++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 e2e/client/playwright/utils/inputs.tsx diff --git a/e2e/client/playwright/multiedit.spec.ts b/e2e/client/playwright/multiedit.spec.ts index 9b492310dd..73e5919230 100644 --- a/e2e/client/playwright/multiedit.spec.ts +++ b/e2e/client/playwright/multiedit.spec.ts @@ -3,6 +3,7 @@ import {Monitoring} from './page-object-models/monitoring'; import {Authoring} from './page-object-models/authoring'; import {MultiEdit} from './page-object-models/multiedit'; import {restoreDatabaseSnapshot, s} from './utils'; +import {clearInput} from './utils/inputs'; test.describe('Multiedit', async () => { test('editing articles in multi-edit mode', async ({page}) => { @@ -15,10 +16,12 @@ test.describe('Multiedit', async () => { await monitoring.executeBulkAction('Multi-edit', ['test sports story', 'story 2']); - await page - .locator(s('multiedit-screen', 'multiedit-article=test sports story', 'field--headline')) - .getByRole('textbox') - .clear(); + await clearInput( + page, + page.locator(s('multiedit-screen', 'multiedit-article=test sports story', 'field--headline')) + .getByRole('textbox'), + ); + await page .locator(s('multiedit-screen', 'multiedit-article=test sports story', 'field--headline')) .getByRole('textbox') @@ -26,10 +29,12 @@ test.describe('Multiedit', async () => { await multiedit.save('test sports story'); - await page - .locator(s('multiedit-screen', 'multiedit-article=story 2', 'field--headline')) - .getByRole('textbox') - .clear(); + await clearInput( + page, + page.locator(s('multiedit-screen', 'multiedit-article=story 2', 'field--headline')) + .getByRole('textbox'), + ); + await page .locator(s('multiedit-screen', 'multiedit-article=story 2', 'field--headline')) .getByRole('textbox') diff --git a/e2e/client/playwright/utils/inputs.tsx b/e2e/client/playwright/utils/inputs.tsx new file mode 100644 index 0000000000..600cfd29ab --- /dev/null +++ b/e2e/client/playwright/utils/inputs.tsx @@ -0,0 +1,10 @@ +import {Page, Locator} from '@playwright/test'; + +/** + * .clear method from playwright doesn't work in a stable manner for editor3 inputs + */ +export async function clearInput(page: Page, textInputLocator: Locator): Promise { + await textInputLocator.focus(); + await page.keyboard.press('Meta+A'); + await page.keyboard.press('Backspace'); +} \ No newline at end of file diff --git a/scripts/core/get-superdesk-api-implementation.tsx b/scripts/core/get-superdesk-api-implementation.tsx index 3f506a63a2..02950c3a90 100644 --- a/scripts/core/get-superdesk-api-implementation.tsx +++ b/scripts/core/get-superdesk-api-implementation.tsx @@ -119,6 +119,7 @@ import {PreviewFieldType} from 'apps/authoring/preview/previewFieldByType'; import {getLabelNameResolver} from 'apps/workspace/helpers/getLabelForFieldId'; import {getSortedFields, getSortedFieldsFiltered} from 'apps/authoring/preview/utils'; import {editor3ToOperationalFormat} from 'apps/authoring-react/fields/editor3'; +import {prepareSuperdeskQuery} from './helpers/universal-query'; function getContentType(id): Promise { return dataApi.findOne('content_types', id); @@ -291,8 +292,9 @@ export function getSuperdeskApiImplementation( getContentStateFromHtml: (html) => getContentStateFromHtml(html), tryLocking, tryUnlocking, - superdeskToElasticQuery: toElasticQuery, getArticleLabel, + superdeskToElasticQuery: toElasticQuery, + prepareSuperdeskQuery: prepareSuperdeskQuery, }, httpRequestJsonLocal, httpRequestRawLocal, diff --git a/scripts/core/superdesk-api.d.ts b/scripts/core/superdesk-api.d.ts index 1e1c3e7604..98a15950ff 100644 --- a/scripts/core/superdesk-api.d.ts +++ b/scripts/core/superdesk-api.d.ts @@ -2952,7 +2952,17 @@ declare module 'superdesk-api' { language: string, ): IEditor3Output; getContentStateFromHtml(html: string): import('draft-js').ContentState; + + /** + * @deprecated + * use prepareSuperdeskQuery + */ superdeskToElasticQuery(q: ISuperdeskQuery): {q?: string, source: string}; + + /** + * endpoint must start with `/` e.g. '/archive' + */ + prepareSuperdeskQuery(endpoint: string, query: ISuperdeskQuery): IHttpRequestOptionsLocal & {method: 'GET'}; }, components: { UserHtmlSingleLine: React.ComponentType<{html: string}>; From fd4956d11bf98ba18ec96dbbd9df7ff600bb9cb7 Mon Sep 17 00:00:00 2001 From: Nikola Stojanovic <68916411+dzonidoo@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:07:46 +0200 Subject: [PATCH 5/8] Update datetime formatting (#4582) --- scripts/appConfig.ts | 12 ++++ scripts/apps/authoring-react/data-layer.ts | 6 +- .../apps/search/components/fields/embargo.tsx | 4 +- .../apps/search/components/fields/state.tsx | 5 +- .../apps/search/components/fields/used.tsx | 4 +- scripts/core/datetime/datetime.ts | 39 ++++++++----- .../core/get-superdesk-api-implementation.tsx | 55 ++++++++++++------- scripts/core/superdesk-api.d.ts | 2 +- 8 files changed, 83 insertions(+), 44 deletions(-) diff --git a/scripts/appConfig.ts b/scripts/appConfig.ts index cf89498749..1a8b8356a6 100644 --- a/scripts/appConfig.ts +++ b/scripts/appConfig.ts @@ -12,6 +12,18 @@ if (appConfig.shortTimeFormat == null) { appConfig.shortTimeFormat = 'HH:mm'; // 24h format } +if (appConfig.view.dateformat == null) { + appConfig.view.dateformat = 'MM/DD'; +} + +if (appConfig.view.timeformat == null) { + appConfig.view.timeformat = 'hh:mm'; +} + +if (appConfig.longDateFormat == null) { + appConfig.longDateFormat = 'LLL'; +} + if (appConfig.ui == null) { appConfig.ui = {}; diff --git a/scripts/apps/authoring-react/data-layer.ts b/scripts/apps/authoring-react/data-layer.ts index fbce850b35..09e03d4c5d 100644 --- a/scripts/apps/authoring-react/data-layer.ts +++ b/scripts/apps/authoring-react/data-layer.ts @@ -25,7 +25,7 @@ import {getArticleAdapter} from './article-adapter'; import {gettext} from 'core/utils'; import {PACKAGE_ITEMS_FIELD_ID} from './fields/package-items'; import {description_text} from './field-adapters/description_text'; -import moment from 'moment'; +import {formatDateTime} from 'core/get-superdesk-api-implementation'; export function getArticleContentProfile( item: IArticle, @@ -421,8 +421,8 @@ export const authoringStorageIArticleCorrect: IAuthoringStorage = { newItem.sms_message = ''; const {override_ednote_for_corrections, override_ednote_template} = appConfig; - const date = moment(newItem.versioncreated) - .format(appConfig.view.dateformat + ' ' + appConfig.view.timeformat); + + const date = formatDateTime(newItem.versioncreated); if (override_ednote_for_corrections && override_ednote_template == null) { const lineBreak = '\r\n\r\n'; diff --git a/scripts/apps/search/components/fields/embargo.tsx b/scripts/apps/search/components/fields/embargo.tsx index 483b6b87b1..a5ddcbd469 100644 --- a/scripts/apps/search/components/fields/embargo.tsx +++ b/scripts/apps/search/components/fields/embargo.tsx @@ -1,8 +1,8 @@ import React from 'react'; import moment from 'moment'; import {gettext} from 'core/utils'; -import {longFormat} from 'core/datetime/datetime'; import {IPropsItemListInfo} from '../ListItemInfo'; +import {formatDate} from 'core/get-superdesk-api-implementation'; class EmbargoComponent extends React.PureComponent { render() { @@ -23,7 +23,7 @@ class EmbargoComponent extends React.PureComponent { key="embargo" className="state-label state_embargo" title={embargoed != null ? ( - gettext('Embargo until {{date}}', {date: longFormat(embargoed)}) + gettext('Embargo until {{date}}', {date: formatDate(embargoed, {longFormat: true})}) ) : ( gettext('Embargo: {{text}}', {text: embargoedText}) )} diff --git a/scripts/apps/search/components/fields/state.tsx b/scripts/apps/search/components/fields/state.tsx index 8abc16ef6c..21d80e416c 100644 --- a/scripts/apps/search/components/fields/state.tsx +++ b/scripts/apps/search/components/fields/state.tsx @@ -1,10 +1,9 @@ import React from 'react'; import {gettext} from 'core/utils'; import {IPropsItemListInfo} from '../ListItemInfo'; -import {longFormat} from 'core/datetime/datetime'; import {assertNever} from 'core/helpers/typescript-helpers'; import {ITEM_STATE} from 'apps/search/interfaces'; -import {openArticle} from 'core/get-superdesk-api-implementation'; +import {formatDate, openArticle} from 'core/get-superdesk-api-implementation'; export function getStateLabel(itemState: ITEM_STATE) { switch (itemState) { @@ -43,7 +42,7 @@ export class StateComponent extends React.Component { label: boolean = false; @@ -9,7 +9,7 @@ export class Used extends React.PureComponent { const item = this.props.item; if (item.used) { - const title = item.used_updated ? longFormat(item.used_updated) : null; + const title = item.used_updated ? formatDate(item.used_updated, {longFormat: true}) : null; return (
    diff --git a/scripts/core/datetime/datetime.ts b/scripts/core/datetime/datetime.ts index ea955b066b..91f4af2967 100644 --- a/scripts/core/datetime/datetime.ts +++ b/scripts/core/datetime/datetime.ts @@ -3,6 +3,7 @@ import {gettext} from 'core/utils'; import moment from 'moment-timezone'; import {appConfig} from 'appConfig'; import {IArticle} from 'superdesk-api'; +import {formatDate} from 'core/get-superdesk-api-implementation'; const ISO_DATE_FORMAT = 'YYYY-MM-DD'; const ISO_WEEK_FORMAT = 'YYYY-W'; @@ -20,9 +21,6 @@ const SERVER_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ'; * * @param {String} d iso format datetime */ -export function longFormat(d: string | moment.Moment): string { - return moment(d).format(LONG_FORMAT); -} export function serverFormat(d: string | moment.Moment): string { return moment(d).utc().format(SERVER_FORMAT); @@ -106,23 +104,36 @@ export function scheduledFormat(__item: IArticle): {short: string, long: string} const item = __item.archive_item ?? __item; - const datetime = item?.schedule_settings?.time_zone == null - ? moment(item.publish_schedule).tz(browserTimezone) - : moment.tz( - item.publish_schedule.replace('+0000', ''), - item.schedule_settings.time_zone, - ).tz(browserTimezone); + const datetime: {moment: moment.Moment, str: string} = (() => { + if (item?.schedule_settings?.time_zone == null) { + const momentObj = moment(item.publish_schedule).tz(browserTimezone); + + return { + moment: momentObj, + str: formatDate(momentObj), + }; + } else { + const momentObj = moment + .tz(item.publish_schedule.replace('+0000', ''), item.schedule_settings.time_zone) + .tz(browserTimezone); + + return { + moment: momentObj, + str: formatDate(momentObj), + }; + } + })(); var now = moment(); - const _date = datetime.format(appConfig.view.dateformat || 'MM/DD'), - _time = datetime.format(appConfig.view.timeformat || 'hh:mm'); + const _date = datetime.str, + _time = datetime.moment.format(appConfig.view.timeformat); - let short = isSameDay(datetime, now) ? '@ '.concat(_time) : _date.concat(' @ ', _time); + let short = isSameDay(datetime.moment, now) ? '@ '.concat(_time) : _date.concat(' @ ', _time); return { short: short, - long: longFormat(datetime), + long: formatDate(datetime.moment, {longFormat: true}), }; } @@ -151,7 +162,7 @@ function DateTimeService() { return m.format(DATE_FORMAT); }; - this.longFormat = longFormat; + this.longFormat = (date) => formatDate(date, {longFormat: true}); } DateTimeHelperService.$inject = []; diff --git a/scripts/core/get-superdesk-api-implementation.tsx b/scripts/core/get-superdesk-api-implementation.tsx index 02950c3a90..47a47c89ad 100644 --- a/scripts/core/get-superdesk-api-implementation.tsx +++ b/scripts/core/get-superdesk-api-implementation.tsx @@ -221,11 +221,41 @@ export function isArticleLockedInCurrentSession(article: IArticle): boolean { return ng.get('lock').isLockedInCurrentSession(article); } -export const formatDate = (date: Date | string) => ( - moment(date) - .tz(appConfig.default_timezone) - .format(appConfig.view.dateformat) -); +export const formatDate = ( + date: Date | string | moment.Moment, + options?: {timezoneId?: string; longFormat?:boolean}, +): string => { + const momentDate = moment.isMoment(date) === true ? date as moment.Moment : moment(date); + const dateFormat = options.longFormat === true ? appConfig.longDateFormat : appConfig.view.dateformat; + + if (options.timezoneId != null) { + return momentDate + .tz(options.timezoneId) + .format(dateFormat); + } else { + const timezone: 'browser' | 'server' = appConfig.view.timezone ?? 'browser'; + const keepLocalTime = timezone === 'browser'; + + return momentDate + .tz(appConfig.default_timezone, keepLocalTime) + .format(dateFormat); + } +}; + +export const formatDateTime = (date: Date, timezoneId?: string) => { + if (timezoneId != null) { + return moment(date) + .tz(timezoneId) + .format(appConfig.view.dateformat + ' ' + appConfig.view.timeformat); + } else { + const timezone: 'browser' | 'server' = appConfig.view.timezone ?? 'browser'; + const keepLocalTime = timezone === 'browser'; + + return moment(date) + .tz(appConfig.default_timezone, keepLocalTime) + .format(appConfig.view.dateformat + ' ' + appConfig.view.timeformat); + } +}; export function dateToServerString(date: Date) { return date.toISOString().slice(0, 19) + '+0000'; @@ -491,20 +521,7 @@ export function getSuperdeskApiImplementation( gettext: (message, params) => gettext(message, params), gettextPlural: (count, singular, plural, params) => gettextPlural(count, singular, plural, params), formatDate: formatDate, - formatDateTime: (date: Date, timezoneId?: string) => { - if (timezoneId != null) { - return moment(date) - .tz(timezoneId) - .format(appConfig.view.dateformat + ' ' + appConfig.view.timeformat); - } else { - const timezone: 'browser' | 'server' = appConfig.view.timezone ?? 'browser'; - const keepLocalTime = timezone === 'browser'; - - return moment(date) - .tz(appConfig.default_timezone, keepLocalTime) - .format(appConfig.view.dateformat + ' ' + appConfig.view.timeformat); - } - }, + formatDateTime: formatDateTime, longFormatDateTime: (date: Date | string, timezoneId?: string) => { if (timezoneId != null) { return moment(date) diff --git a/scripts/core/superdesk-api.d.ts b/scripts/core/superdesk-api.d.ts index 98a15950ff..eff55e89a9 100644 --- a/scripts/core/superdesk-api.d.ts +++ b/scripts/core/superdesk-api.d.ts @@ -3062,7 +3062,7 @@ declare module 'superdesk-api' { localization: { gettext(message: string, params?: {[placeholder: string]: string | number | React.ComponentType}): string; gettextPlural(count: number, singular: string, plural: string, params?: {[placeholder: string]: string | number | React.ComponentType}): string; - formatDate(date: Date | string): string; + formatDate(date: Date | string | moment.Moment, options?: {timezoneId?: string; longFormat?:boolean}): string; formatDateTime(date: Date, timezoneId?: string): string; longFormatDateTime(date: Date | string, timezoneId?: string): string; getRelativeOrAbsoluteDateTime( From 3d6fd29ec4048b8bbf310b621691466dd4075894 Mon Sep 17 00:00:00 2001 From: Tomas Kikutis Date: Thu, 15 Aug 2024 15:48:22 +0200 Subject: [PATCH 6/8] Content profile icons (#4594) * content profile edit view supports custom icon * groundwork * add contentProfileId to TypeIcon * add content profile filters next to content type filters * add e2e test * fix lint issues * fix compilation error importing IScope from angular in utils was crashing it * update existing API to be sync * remove useless navigation * see if this fixes e2e --- e2e/client/playwright/content-profile.spec.ts | 49 ++++++++++++++++ scripts/api/content-profiles.ts | 6 +- scripts/apps/desks/views/settings.html | 1 - .../components/AssignmentsComponent.tsx | 2 +- .../components/ListItemsComponent.tsx | 1 + .../monitoring/controllers/AggregateCtrl.ts | 23 +++++++- .../monitoring/views/monitoring-view.html | 15 ++++- .../apps/search/components/Associations.tsx | 5 +- .../apps/search/components/GridTypeIcon.tsx | 26 ++------- .../apps/search/components/ListTypeIcon.tsx | 1 + scripts/apps/search/components/TypeIcon.tsx | 23 +++++++- scripts/apps/search/components/WidgetItem.tsx | 3 +- .../apps/search/components/fields/type.tsx | 8 +-- scripts/apps/search/constants.ts | 2 + .../controllers/ContentProfilesController.ts | 6 ++ .../content/views/profile-settings.html | 11 ++-- scripts/core/ArticlesListV2MultiSelect.tsx | 1 + scripts/core/editor3/directive.tsx | 3 +- scripts/core/find-parent-scope.ts | 13 +++++ .../core/get-superdesk-api-implementation.tsx | 27 +-------- scripts/core/helpers/data-provider.ts | 6 +- scripts/core/helpers/fetch-all.tsx | 8 ++- scripts/core/superdesk-api.d.ts | 14 +++-- .../ui/components/article-item-concise.tsx | 1 + scripts/core/ui/ui.ts | 11 +++- scripts/core/utils.tsx | 13 ----- scripts/data-store.ts | 57 +++++++++++++++++++ scripts/index.ts | 2 + 28 files changed, 247 insertions(+), 91 deletions(-) create mode 100644 e2e/client/playwright/content-profile.spec.ts create mode 100644 scripts/core/find-parent-scope.ts create mode 100644 scripts/data-store.ts diff --git a/e2e/client/playwright/content-profile.spec.ts b/e2e/client/playwright/content-profile.spec.ts new file mode 100644 index 0000000000..37c820dc3c --- /dev/null +++ b/e2e/client/playwright/content-profile.spec.ts @@ -0,0 +1,49 @@ +import {test, expect} from '@playwright/test'; +import {Monitoring} from './page-object-models/monitoring'; +import {restoreDatabaseSnapshot, s} from './utils'; + +test('content profile icon', async ({page}) => { + const monitoring = new Monitoring(page); + + await restoreDatabaseSnapshot(); + + // expect an article to have a regular text icon + + await page.goto('/#/workspace/monitoring'); + await monitoring.selectDeskOrWorkspace('Sports'); + + await expect( + page.locator(s( + 'monitoring-group=Sports / Working Stage', + 'article-item=test sports story', + 'type-icon', + )), + ).toHaveAttribute('data-test-value', 'text'); + + + // go to content profile and set icon to "map-marker" + + await page.goto('/#/settings/content-profiles'); + await page.locator(s('content-profile=Story', 'content-profile-actions')).click(); + await page.locator(s('content-profile-actions--options')).getByRole('button', {name: 'Edit'}).click(); + + await page.locator(s('content-profile-edit-view')).getByLabel('Icon').getByRole('button').click(); + await page.getByRole('button', {name: 'map-marker'}).click(); + await page.locator(s('content-profile-edit-view--footer')).getByRole('button', {name: 'Save'}).click(); + + await expect(page.locator(s('content-profile=Story', 'icon'))).toHaveAttribute('data-test-value', 'map-marker'); + + + // go back to monitoring and test whether the newly set icon is being used + + await page.goto('/#/workspace/monitoring'); + await monitoring.selectDeskOrWorkspace('Sports'); + + await expect( + page.locator(s( + 'monitoring-group=Sports / Working Stage', + 'article-item=test sports story', + 'type-icon', + )), + ).toHaveAttribute('data-test-value', 'map-marker', {timeout: 10000}); +}); diff --git a/scripts/api/content-profiles.ts b/scripts/api/content-profiles.ts index d537be47be..f14cc8f788 100644 --- a/scripts/api/content-profiles.ts +++ b/scripts/api/content-profiles.ts @@ -1,10 +1,10 @@ import {IContentProfile} from 'superdesk-api'; -import ng from 'core/services/ng'; +import {dataStore} from 'data-store'; interface IContentProfilesApi { - get(id: IContentProfile['_id']): Promise; + get(id: IContentProfile['_id']): IContentProfile; } export const contentProfiles: IContentProfilesApi = { - get: (id) => ng.get('content').getType(id), + get: (id) => dataStore.contentProfiles.get(id), }; diff --git a/scripts/apps/desks/views/settings.html b/scripts/apps/desks/views/settings.html index 77ab3380fa..e5fc06b83b 100644 --- a/scripts/apps/desks/views/settings.html +++ b/scripts/apps/desks/views/settings.html @@ -32,7 +32,6 @@

    Desk management

    diff --git a/scripts/apps/monitoring/controllers/AggregateCtrl.ts b/scripts/apps/monitoring/controllers/AggregateCtrl.ts index 81adddc862..ae7cc45304 100644 --- a/scripts/apps/monitoring/controllers/AggregateCtrl.ts +++ b/scripts/apps/monitoring/controllers/AggregateCtrl.ts @@ -2,7 +2,7 @@ import {each, forEach, isNil, partition, keyBy} from 'lodash'; import {gettext, getItemTypes} from 'core/utils'; import {SCHEDULED_OUTPUT, DESK_OUTPUT} from 'apps/desks/constants'; import {appConfig} from 'appConfig'; -import {IMonitoringFilter, IStage, IDesk, IMonitoringGroup} from 'superdesk-api'; +import {IMonitoringFilter, IStage, IDesk, IMonitoringGroup, IContentProfile} from 'superdesk-api'; import {getLabelForStage} from 'apps/workspace/content/constants'; import {getExtensionSections} from '../services/CardsService'; @@ -409,6 +409,27 @@ export function AggregateCtrl($scope, desks, workspaces, preferencesService, sto $scope.$apply(); }; + this.toggleContentProfileFilter = (profile: IContentProfile) => { + if (this.isContentProfileFilterActive(profile._id)) { + this.activeFilterTags[CONTENT_PROLFILE] = + this.activeFilterTags[CONTENT_PROLFILE].filter(({key}) => key !== profile._id); + } else { + this.activeFilterTags[CONTENT_PROLFILE] = (this.activeFilterTags[CONTENT_PROLFILE] ?? []).concat({ + key: profile._id, + label: profile.label, + }); + } + + this.activeFilters.contentProfile = this.activeFilterTags[CONTENT_PROLFILE].map(({key}) => key); + + updateFilterInStore(); + updateFilteringCriteria(); + }; + + this.isContentProfileFilterActive = (id: IContentProfile['_id']): boolean => { + return (this.activeFilterTags[CONTENT_PROLFILE] ?? []).find(({key}) => key === id) != null; + }; + this.setCustomFilter = (filter: IMonitoringFilter) => { if (typeof this.activeFilters.customFilters === 'undefined') { this.activeFilters.customFilters = {}; diff --git a/scripts/apps/monitoring/views/monitoring-view.html b/scripts/apps/monitoring/views/monitoring-view.html index c517e76289..77f7be72e1 100644 --- a/scripts/apps/monitoring/views/monitoring-view.html +++ b/scripts/apps/monitoring/views/monitoring-view.html @@ -153,7 +153,20 @@
    diff --git a/scripts/apps/search/components/Associations.tsx b/scripts/apps/search/components/Associations.tsx index d712cbb48a..d035072bf3 100644 --- a/scripts/apps/search/components/Associations.tsx +++ b/scripts/apps/search/components/Associations.tsx @@ -39,7 +39,10 @@ export class Associations extends React.Component { onClick={this.openItem} title={gettext('Associated ') + this.props.item.associations.featuremedia.type} > - +
    ); } diff --git a/scripts/apps/search/components/GridTypeIcon.tsx b/scripts/apps/search/components/GridTypeIcon.tsx index d47138aea1..441e24fa27 100644 --- a/scripts/apps/search/components/GridTypeIcon.tsx +++ b/scripts/apps/search/components/GridTypeIcon.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import {TypeIcon} from './index'; import classNames from 'classnames'; import {IArticle} from 'superdesk-api'; @@ -10,25 +9,12 @@ interface IProps { swimlane?: any; } -export const GridTypeIcon: React.StatelessComponent = (props) => { - if (props.photoGrid) { - return React.createElement('span', - {className: classNames('sd-grid-item__type-icn', - {swimlane: props.swimlane}, - )}, - React.createElement(TypeIcon, {type: props.item.type}), - ); - } +export const GridTypeIcon: React.FunctionComponent = (props) => { + const className = props.photoGrid ? classNames('sd-grid-item__type-icn', {swimlane: props.swimlane}) : undefined; - return React.createElement( - 'span', - {}, - React.createElement(TypeIcon, {type: props.item.type}), + return ( + + + ); }; - -GridTypeIcon.propTypes = { - swimlane: PropTypes.any, - item: PropTypes.any, - photoGrid: PropTypes.bool, -}; diff --git a/scripts/apps/search/components/ListTypeIcon.tsx b/scripts/apps/search/components/ListTypeIcon.tsx index 620c157331..2cad638dbd 100644 --- a/scripts/apps/search/components/ListTypeIcon.tsx +++ b/scripts/apps/search/components/ListTypeIcon.tsx @@ -64,6 +64,7 @@ export class ListTypeIcon extends React.Component { : ( diff --git a/scripts/apps/search/components/TypeIcon.tsx b/scripts/apps/search/components/TypeIcon.tsx index af9a76c09e..2c7be962d1 100644 --- a/scripts/apps/search/components/TypeIcon.tsx +++ b/scripts/apps/search/components/TypeIcon.tsx @@ -1,7 +1,9 @@ import React from 'react'; import {gettext} from 'core/utils'; +import {dataStore} from 'data-store'; interface IProps { + contentProfileId: string; type: string; highlight?: boolean; 'aria-hidden'?: boolean; @@ -12,7 +14,23 @@ interface IProps { */ export class TypeIcon extends React.PureComponent { render() { - const {type, highlight} = this.props; + const {type, highlight, contentProfileId} = this.props; + + if (contentProfileId != null) { + const profile = dataStore.contentProfiles.get(contentProfileId); + + if (profile?.icon != null) { + return ( + + ); + } + } if (type === 'composite' && highlight) { return ( @@ -20,6 +38,7 @@ export class TypeIcon extends React.PureComponent { className={'filetype-icon-highlight-pack'} aria-label={gettext('Article Type {{type}}', {type})} aria-hidden={this.props['aria-hidden'] ?? false} + data-test-id="type-icon" /> ); } @@ -30,6 +49,8 @@ export class TypeIcon extends React.PureComponent { title={gettext('Article Type: {{type}}', {type})} aria-label={gettext('Article Type {{type}}', {type})} aria-hidden={this.props['aria-hidden'] ?? false} + data-test-id="type-icon" + data-test-value={type} /> ); } diff --git a/scripts/apps/search/components/WidgetItem.tsx b/scripts/apps/search/components/WidgetItem.tsx index ff58acf895..81be425450 100644 --- a/scripts/apps/search/components/WidgetItem.tsx +++ b/scripts/apps/search/components/WidgetItem.tsx @@ -23,7 +23,7 @@ interface IProps { * @description This component is a row in monitoring widget item list. */ export class WidgetItem extends React.Component { - item: any; + item: IArticle; constructor(props) { super(props); @@ -70,6 +70,7 @@ export class WidgetItem extends React.Component {
    diff --git a/scripts/apps/search/components/fields/type.tsx b/scripts/apps/search/components/fields/type.tsx index 03bae9c58e..358894034b 100644 --- a/scripts/apps/search/components/fields/type.tsx +++ b/scripts/apps/search/components/fields/type.tsx @@ -4,17 +4,15 @@ import {IPropsItemListInfo} from '../ListItemInfo'; class TypeComponent extends React.Component { render() { - const props = this.props; + const item = this.props.item; - if (props.item.type == null) { + if (item.type == null) { return null; } - const {_type, highlight} = props.item; - return ( - + ); } diff --git a/scripts/apps/search/constants.ts b/scripts/apps/search/constants.ts index a3728e1836..e796c22691 100644 --- a/scripts/apps/search/constants.ts +++ b/scripts/apps/search/constants.ts @@ -184,6 +184,8 @@ export const CORE_PROJECTED_FIELDS = { 'translated_from', 'translations', 'schedule_settings', + + 'profile', ], }; diff --git a/scripts/apps/workspace/content/controllers/ContentProfilesController.ts b/scripts/apps/workspace/content/controllers/ContentProfilesController.ts index 5284fb6568..abddcf8e33 100644 --- a/scripts/apps/workspace/content/controllers/ContentProfilesController.ts +++ b/scripts/apps/workspace/content/controllers/ContentProfilesController.ts @@ -252,6 +252,12 @@ export function ContentProfilesController($scope: IScope, $location, notify, con .then(this.toggleEdit); }; + this.onIconChange = (val) => { + $scope.editing.form.icon = val; + $scope.ngForm.$dirty = true; + $scope.$apply(); + }; + /** * @description Commits the changes made in the editing form for a profile * to the server. diff --git a/scripts/apps/workspace/content/views/profile-settings.html b/scripts/apps/workspace/content/views/profile-settings.html index 021b63fe36..9757f53824 100644 --- a/scripts/apps/workspace/content/views/profile-settings.html +++ b/scripts/apps/workspace/content/views/profile-settings.html @@ -44,12 +44,13 @@

    Content Profiles

    - + + {{ type.label}}
    @@ -139,12 +140,12 @@