diff --git a/e2e/client/playwright/article-send-to.spec.ts b/e2e/client/playwright/article-send-to.spec.ts new file mode 100644 index 0000000000..31925b3c13 --- /dev/null +++ b/e2e/client/playwright/article-send-to.spec.ts @@ -0,0 +1,71 @@ +import {test, expect} from '@playwright/test'; +import {Monitoring} from './page-object-models/monitoring'; +import {restoreDatabaseSnapshot, s} from './utils'; +import {TreeSelectDriver} from './utils/tree-select-driver'; + +test.describe('sending an article', async () => { + test('sending an article to another desk', async ({page}) => { + const monitoring = new Monitoring(page); + + await restoreDatabaseSnapshot(); + await page.goto('/#/workspace/monitoring'); + await monitoring.selectDeskOrWorkspace('Sports'); + + await monitoring.executeActionOnMonitoringItem( + page.locator(s('monitoring-group=Sports / Working Stage', 'article-item=story 2')), + 'Edit', + ); + + await page.locator(s('authoring-topbar', 'open-send-publish-pane')).click(); + await page.locator(s('interactive-actions-panel', 'tabs')).getByRole('tab', {name: 'Send to'}).click(); + + // selecting other desk + await new TreeSelectDriver( + page, + page.locator(s('destination-select')), + ).setValue(['Educations']); + await page + .locator(s('interactive-actions-panel', 'stage-select')) + .getByRole('radio', {name: 'Working Stage'}) + .check(); + await page.locator(s('interactive-actions-panel', 'send')).click(); + + await expect( + page.locator(s('monitoring-group=Sports / Working Stage', 'article-item=story 2')), + ).not.toBeVisible(); + await monitoring.selectDeskOrWorkspace('Educations'); + await expect( + page.locator(s('monitoring-group=Educations / Working Stage', 'article-item=story 2')), + ).toBeVisible(); + }); + + test('sending an article to another stage', async ({page}) => { + const monitoring = new Monitoring(page); + + await restoreDatabaseSnapshot(); + await page.goto('/#/workspace/monitoring'); + await monitoring.selectDeskOrWorkspace('Sports'); + + await monitoring.executeActionOnMonitoringItem( + page.locator(s('monitoring-group=Sports / Working Stage', 'article-item=story 2')), + 'Edit', + ); + + await page.locator(s('authoring-topbar', 'open-send-publish-pane')).click(); + await page.locator(s('interactive-actions-panel', 'tabs')).getByRole('tab', {name: 'Send to'}).click(); + + // selecting other stage + await page + .locator(s('interactive-actions-panel', 'stage-select')) + .getByRole('radio', {name: 'Incoming Stage'}) + .check(); + await page.locator(s('interactive-actions-panel', 'send')).click(); + + await expect( + page.locator(s('monitoring-group=Sports / Working Stage', 'article-item=story 2')), + ).not.toBeVisible(); + await expect( + page.locator(s('monitoring-group=Sports / Incoming Stage', 'article-item=story 2')), + ).toBeVisible(); + }); +}); diff --git a/package-lock.json b/package-lock.json index d6168ad806..e722ed2eaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -818,7 +818,7 @@ }, "@typescript-eslint/parser": { "version": "5.57.0", - "resolved": "https://verdaccio.sourcefabric.org/@typescript-eslint/parser/-/parser-5.57.0.tgz", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.0.tgz", "integrity": "sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==", "requires": { "@typescript-eslint/scope-manager": "5.57.0", @@ -829,7 +829,7 @@ "dependencies": { "debug": { "version": "4.3.4", - "resolved": "https://verdaccio.sourcefabric.org/debug/-/debug-4.3.4.tgz", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" @@ -1315,7 +1315,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "prop-types": { "version": "15.8.1", @@ -8473,7 +8473,7 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" }, "json-loader": { "version": "0.5.7", @@ -12028,7 +12028,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" } } }, @@ -12523,7 +12523,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "prop-types": { "version": "15.7.2", @@ -12810,7 +12810,7 @@ "regexpu-core": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", - "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "integrity": "sha512-Ci+lDRlvAElKjFp5keqmVUaJLqZiHywekXhshT6wVUyDObGPdymNPhxBmf38ZVsaUGOnZ3Fot9YzxvoI31ymYw==", "requires": { "regenerate": "^1.2.1", "regjsgen": "^0.2.0", @@ -12820,12 +12820,12 @@ "regjsgen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + "integrity": "sha512-x+Y3yA24uF68m5GA+tBjbGYo64xXVJpbToBaWCoSNSc1hdk6dfctaRWrNFTVJZIIhL5GxW8zwjoixbnifnK59g==" }, "regjsparser": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "integrity": "sha512-jlQ9gYLfk2p3V5Ag5fYhA7fv7OHzd1KUH0PRP46xc3TgwjwgROIW572AfYg/X9kaNq/LJnu6oJcFRXlIrGoTRw==", "requires": { "jsesc": "~0.5.0" } diff --git a/package.json b/package.json index 3cf9ffc63f..85d33d7d81 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "sass-loader": "6.0.6", "shortid": "2.2.8", "style-loader": "0.20.2", - "superdesk-ui-framework": "^3.1.9", + "superdesk-ui-framework": "3.1.9", "ts-loader": "3.5.0", "typescript": "4.9.5", "uuid": "8.3.1", diff --git a/scripts/apps/archive/directives/HtmlPreview.ts b/scripts/apps/archive/directives/HtmlPreview.ts index 51e4e5fdcc..d6111a41ab 100644 --- a/scripts/apps/archive/directives/HtmlPreview.ts +++ b/scripts/apps/archive/directives/HtmlPreview.ts @@ -1,3 +1,4 @@ +import {adjustHTMLForPreview} from 'apps/authoring/preview/field-types/html'; import {getAnnotationsFromItem} from 'core/editor3/helpers/editor3CustomData'; import {META_FIELD_NAME} from 'core/editor3/helpers/fieldsMeta'; import ng from 'core/services/ng'; @@ -60,7 +61,7 @@ export function HtmlPreview($sce, $timeout) { templateUrl: 'scripts/apps/archive/views/html-preview.html', link: function(scope, elem, attrs) { scope.$watch('sdHtmlPreview', (html) => { - scope.html = $sce.trustAsHtml(html); + scope.html = $sce.trustAsHtml(adjustHTMLForPreview(html)); if (window.hasOwnProperty('instgrm')) { window.instgrm.Embeds.process(); diff --git a/scripts/apps/archive/related-item-widget/relatedItem.ts b/scripts/apps/archive/related-item-widget/relatedItem.ts index 674456d4e9..28655a53b6 100644 --- a/scripts/apps/archive/related-item-widget/relatedItem.ts +++ b/scripts/apps/archive/related-item-widget/relatedItem.ts @@ -104,14 +104,14 @@ function RelatedItemController( .then((items) => { if (items && items._items && items._items.length > 1) { $scope.options.existingRelations = items._items; - $scope.widget.configurable = false; + $scope.active.configurable = false; $scope.options.searchEnabled = false; - $scope.widget.label = gettext('Related Items'); + $scope.active.label = gettext('Related Items'); } else { $scope.options.existingRelations = false; - $scope.widget.configurable = true; + $scope.active.configurable = true; $scope.options.searchEnabled = true; - $scope.widget.label = gettext('Relate an item'); + $scope.active.label = gettext('Relate an item'); } }) .finally(() => { @@ -242,16 +242,16 @@ function RelatedItemController( }, true); function reset() { - if ($scope.widget && $scope.widget.configuration) { - $scope.widget.configuration.modificationDateAfter = storage.getItem('modificationDateAfter') || 'today'; - $scope.widget.configuration.sluglineMatch = storage.getItem('sluglineMatch') || 'EXACT'; + if ($scope.active && $scope.active.configuration) { + $scope.active.configuration.modificationDateAfter = storage.getItem('modificationDateAfter') || 'today'; + $scope.active.configuration.sluglineMatch = storage.getItem('sluglineMatch') || 'EXACT'; } } - if ($scope.widget) { - $scope.widget.save = function() { - storage.setItem('sluglineMatch', $scope.widget.configuration.sluglineMatch); - storage.setItem('modificationDateAfter', $scope.widget.configuration.modificationDateAfter); + if ($scope.active) { + $scope.active.save = function() { + storage.setItem('sluglineMatch', $scope.active.configuration.sluglineMatch); + storage.setItem('modificationDateAfter', $scope.active.configuration.modificationDateAfter); }; } diff --git a/scripts/apps/authoring/preview/field-types/html.tsx b/scripts/apps/authoring/preview/field-types/html.tsx index 76337c8200..3c2b0ef397 100644 --- a/scripts/apps/authoring/preview/field-types/html.tsx +++ b/scripts/apps/authoring/preview/field-types/html.tsx @@ -1,13 +1,41 @@ +import {sdApi} from 'api'; import React from 'react'; interface IProps { value: string; } +export function adjustHTMLForPreview(html: string): string { + const parsed: HTMLElement = + new DOMParser().parseFromString(html, 'text/html').body; + + parsed.querySelectorAll('[data-custom-block-type]').forEach((element) => { + const customBlockType = element.getAttribute('data-custom-block-type'); + const vocabulary = sdApi.vocabularies.getAll().get(customBlockType); + const separator = '
'; + + element.innerHTML = `
+ ${separator} + +
+ ${vocabulary.display_name} +
+ + ${element.innerHTML} + + ${separator} +
`; + }); + + return parsed.innerHTML; +} + export class HtmlPreview extends React.Component { render() { + const html = this.props.value; + return ( -
+
); } } diff --git a/scripts/apps/authoring/styles/themes.scss b/scripts/apps/authoring/styles/themes.scss index 47aa5a25e7..9fa13f7972 100644 --- a/scripts/apps/authoring/styles/themes.scss +++ b/scripts/apps/authoring/styles/themes.scss @@ -442,7 +442,7 @@ body, html { width: 100%; display: table; table-layout: fixed; - margin: 10px 0; + margin-top: 1.5em; &.item-association { margin: 0; } diff --git a/scripts/apps/authoring/widgets/views/authoring-widgets.html b/scripts/apps/authoring/widgets/views/authoring-widgets.html index cdff5d016d..55bd7ac670 100644 --- a/scripts/apps/authoring/widgets/views/authoring-widgets.html +++ b/scripts/apps/authoring/widgets/views/authoring-widgets.html @@ -1,44 +1,11 @@ -
-
- -
- -
- -
-
-
- - -
-
- -
+ 'right-extended': active.extended + }" +> +
- -
+
-
-
-
diff --git a/scripts/apps/authoring/widgets/widgets.ts b/scripts/apps/authoring/widgets/widgets.ts index 7c46afdab0..3d5f3017a8 100644 --- a/scripts/apps/authoring/widgets/widgets.ts +++ b/scripts/apps/authoring/widgets/widgets.ts @@ -243,7 +243,7 @@ function WidgetsManagerCtrl( }); if (this.widgetFromPreferences) { - let widgetFromPreferences = $scope.widgets.find((widget) => + let widgetFromPreferences = $scope.widgets?.find((widget) => widget._id === this.widgetFromPreferences._id); if (widgetFromPreferences) { @@ -338,6 +338,7 @@ function WidgetsManagerCtrl( } else { angular.element('body').addClass('main-section--pinned-tabs'); $scope.pinnedWidget = widget; + $scope.active = widget; widget.pinned = true; this.updateUserPreferences(widget); @@ -349,7 +350,7 @@ function WidgetsManagerCtrl( widgetReactIntegration.pinWidget = $scope.pinWidget; widgetReactIntegration.getActiveWidget = () => $scope.active ?? $scope.pinnedWidget; widgetReactIntegration.getPinnedWidget = - () => $scope.widgets.find(({pinned}) => pinned === true)?.name ?? null; + () => $scope.widgets?.find(({pinned}) => pinned === true)?.name ?? null; widgetReactIntegration.WidgetHeaderComponent = WidgetHeaderComponent; widgetReactIntegration.WidgetLayoutComponent = WidgetLayoutComponent; @@ -377,7 +378,11 @@ function WidgetsManagerCtrl( $scope.active.afterClose($scope); } - $scope.active = null; + if ($scope.pinnedWidget != null) { + $scope.active = $scope.pinnedWidget; + } else { + $scope.active = null; + } }; // activate widget based on query string diff --git a/scripts/apps/dashboard/widget-react.tsx b/scripts/apps/dashboard/widget-react.tsx index a8c7f5ede2..fcc34b76c1 100644 --- a/scripts/apps/dashboard/widget-react.tsx +++ b/scripts/apps/dashboard/widget-react.tsx @@ -4,13 +4,20 @@ import React from 'react'; import {IArticle, IArticleSideWidget} from 'superdesk-api'; interface IProps { - widget: IArticleSideWidget; + widget: { + active: IArticleSideWidget; + pinnedWidget: IArticleSideWidget; + }; article: IArticle; } -export class WidgetReact extends React.PureComponent { +interface IState { + widgetDisplayed: IArticleSideWidget['component']; +} + +export class WidgetReact extends React.PureComponent { render() { - const Component = this.props.widget.component; + const Component = this.props.widget.active?.component ?? this.props.widget?.pinnedWidget?.component; // Ensure that widget component re-mounts if the item is locked/unlocked. // Avoid null key in case item is unlocked - use a random string to force it to re-mount. @@ -23,7 +30,8 @@ export class WidgetReact extends React.PureComponent { initialState={(() => { const localStorageWidgetState = JSON.parse(localStorage.getItem('SIDE_WIDGET') ?? 'null'); - if (localStorageWidgetState?.id === this.props.widget._id) { + if (localStorageWidgetState?.id + === (this.props.widget.active?._id ?? this.props.widget.pinnedWidget._id)) { const initialState = localStorageWidgetState?.initialState; localStorage.removeItem('SIDE_WIDGET'); diff --git a/scripts/core/editor3/components/custom-block.scss b/scripts/core/editor3/components/custom-block.scss index d734d3f750..f1ad789ef3 100644 --- a/scripts/core/editor3/components/custom-block.scss +++ b/scripts/core/editor3/components/custom-block.scss @@ -1,6 +1,8 @@ .editor3-custom-block { .table-block table td { - border: 1px solid var(--sd-editor-colour__controls-border)!important; + border: 1px solid var(--sd-editor-colour__controls-border) !important; + border-radius: 0 var(--b-radius--large) var(--b-radius--large); + padding: var(--space--1-5) !important; } .table-inside table { @@ -15,7 +17,6 @@ .editor3-custom-block--label { font-size: 11px; font-weight: 500; - color: var(--color-text); text-transform: uppercase; letter-spacing: 0.08em; padding-block-start: 1px; // make text be vertically in the center of the bounding box @@ -26,21 +27,22 @@ --handle-height: 14px; display: flex; - padding: 4px 8px; - padding-inline-start: 4px; // less padding for drag handle - gap: 8px; + padding: var(--space--0-5) var(--space--1); + gap: var(--gap-1); align-items: center; cursor: grab; - border-start-start-radius: 2px; - border-start-end-radius: 2px; - + border-start-start-radius: var(--b-radius--medium); + border-start-end-radius: var(--b-radius--medium); background: var(--sd-editor-colour__controls-border); - + opacity: 0.8; + transition: opacity 0.2s ease; + color: currentColor; &:hover { - background: gray; + opacity: 1; } - &:active { - background:var(--sd-colour-primary); + background-color: var(--sd-colour-interactive); + color: white; + } } diff --git a/scripts/core/editor3/components/toolbar/TableControls.tsx b/scripts/core/editor3/components/toolbar/TableControls.tsx index ff2251f7cf..f2daefc503 100644 --- a/scripts/core/editor3/components/toolbar/TableControls.tsx +++ b/scripts/core/editor3/components/toolbar/TableControls.tsx @@ -111,14 +111,12 @@ const TableControlsComponent: React.FunctionComponent = (props) => { return ( <> { - editorFormat.includes('link') && ( - setTablePopup(PopupTypes.Link, payload)} - iconName="link" - tooltip={gettext('Link')} - /> - ) + setTablePopup(PopupTypes.Link, payload)} + iconName="link" + tooltip={gettext('Link')} + /> } { @@ -129,11 +127,6 @@ const TableControlsComponent: React.FunctionComponent = (props) => { /> ) } - - setTablePopup(PopupTypes.Link, payload)} - /> ); } else if (type in inlineStyles) { @@ -161,6 +154,12 @@ const TableControlsComponent: React.FunctionComponent = (props) => { } }) } + + {/* LinkToolbar must be the last node. */} + setTablePopup(PopupTypes.Link, payload)} + />
); }; diff --git a/scripts/core/editor3/html/to-html/AtomicBlockParser.ts b/scripts/core/editor3/html/to-html/AtomicBlockParser.ts index d43948a2a1..16bdaa6a95 100644 --- a/scripts/core/editor3/html/to-html/AtomicBlockParser.ts +++ b/scripts/core/editor3/html/to-html/AtomicBlockParser.ts @@ -8,6 +8,7 @@ import {CustomEditor3Entity} from 'core/editor3/constants'; import {IEditorDragDropArticleEmbed} from 'core/editor3/reducers/editor3'; import {assertNever} from 'core/helpers/typescript-helpers'; import {sdApi} from 'api'; +import {configurableAlgorithms} from 'core/ui/configurable-algorithms'; /** * @ngdoc class @@ -231,33 +232,24 @@ export class AtomicBlockParser { return ''; } - function getHighestHeadingText(el: HTMLElement): string | null { - const headings: Array = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - - for (const tag of headings) { - const result = el.querySelector(tag); - - if (result != null) { - return result.textContent; - } - } - - return null; - } - + const vocabulary = sdApi.vocabularies.getAll().get(data.vocabularyId); + const blockId = vocabulary._id; const {cells} = data; - const blockName = sdApi.vocabularies.getAll().get(data.vocabularyId).display_name; const cellContentState: ContentState = convertFromRaw(cells[0][0]); const tableCellContentHtml = editor3StateToHtml(cellContentState); - const tableCellContentElement: HTMLElement = - new DOMParser().parseFromString(tableCellContentHtml, 'text/html').body; - const heading: string | null = getHighestHeadingText(tableCellContentElement); - const attributes = [`data-custom-block-type="${blockName}"`]; - if (heading != null) { - attributes.push(`data-custom-block-title="${heading}"`); - } + const attributes: Array<{name: string; value: string}> = [ + {name: 'data-custom-block-type', value: blockId}, + ...( + configurableAlgorithms.editor3?.customBlocks?.getAdditionalWrapperAttributes( + vocabulary, + tableCellContentHtml, + ) ?? [] + ), + ]; + + const attributesString = attributes.map(({name, value}) => `${name}="${value}"`).join(' '); - return `
${tableCellContentHtml}
`; + return `
${tableCellContentHtml}
`; } } diff --git a/scripts/core/editor3/styles.scss b/scripts/core/editor3/styles.scss index daab8a2bd3..43b9541819 100644 --- a/scripts/core/editor3/styles.scss +++ b/scripts/core/editor3/styles.scss @@ -936,7 +936,6 @@ $editor-styleButton-size: 3rem; right: auto; z-index: 10; box-shadow: 0px 1px 5px #00000066; - background-color: #f8f8f8; } .sd-input-style .Editor3-root { diff --git a/scripts/core/superdesk-api.d.ts b/scripts/core/superdesk-api.d.ts index 66fd9bff48..179d804405 100644 --- a/scripts/core/superdesk-api.d.ts +++ b/scripts/core/superdesk-api.d.ts @@ -2034,10 +2034,11 @@ declare module 'superdesk-api' { export interface IConfigurableAlgorithms { countLines?(plainText: string, lineLength: number): number; - } - - export interface IConfigurableAlgorithms { - countLines?(plainText: string, lineLength: number): number; + editor3?: { + customBlocks?: { + getAdditionalWrapperAttributes?(customBlockVocabulary: IVocabulary, html: string): Array<{name: string; value: string}>; + }; + } } export interface IListItemProps {