diff --git a/app/assets/stylesheets/pageflow/editor/base.scss b/app/assets/stylesheets/pageflow/editor/base.scss
index af391ec0f6..35e0e63498 100644
--- a/app/assets/stylesheets/pageflow/editor/base.scss
+++ b/app/assets/stylesheets/pageflow/editor/base.scss
@@ -59,6 +59,7 @@
@import "./composables";
@import "./list";
@import "./drop_down_button";
+ @import "./edit_configuration_view";
@import "./outline";
@import "./sortable";
diff --git a/app/assets/stylesheets/pageflow/editor/drop_down_button.scss b/app/assets/stylesheets/pageflow/editor/drop_down_button.scss
index 81ee6b55dc..f5f5e7e77a 100644
--- a/app/assets/stylesheets/pageflow/editor/drop_down_button.scss
+++ b/app/assets/stylesheets/pageflow/editor/drop_down_button.scss
@@ -12,9 +12,16 @@
> button.ellipsis_icon {
@include fa-ellipsis-v-icon;
+ }
+
+ > button.ellipsis_icon.has_icon_only {
width: var(--drop-down-button-width, 31px);
}
+ > button.ellipsis_icon.has_icon_and_text::before {
+ margin-right: 9px;
+ }
+
> button.borderless {
--ui-button-border-color: transparent;
--ui-button-hover-border-color: transparent;
@@ -102,6 +109,11 @@
padding-top: 1px;
}
+ &.is_destructive a:hover {
+ background-color: var(--ui-error-color);
+ color: var(--ui-on-error-color);
+ }
+
&.has_radio,
&.has_check_box {
a {
diff --git a/app/assets/stylesheets/pageflow/editor/edit_configuration_view.scss b/app/assets/stylesheets/pageflow/editor/edit_configuration_view.scss
new file mode 100644
index 0000000000..612240b395
--- /dev/null
+++ b/app/assets/stylesheets/pageflow/editor/edit_configuration_view.scss
@@ -0,0 +1,5 @@
+.edit_configuration_view {
+ .actions_drop_down_button {
+ float: right;
+ }
+}
diff --git a/app/assets/stylesheets/pageflow/editor/info_box.scss b/app/assets/stylesheets/pageflow/editor/info_box.scss
index daf43e4eda..3bb7896688 100644
--- a/app/assets/stylesheets/pageflow/editor/info_box.scss
+++ b/app/assets/stylesheets/pageflow/editor/info_box.scss
@@ -18,6 +18,18 @@
background-color: var(--ui-error-surface-color);
}
+ .with_icon {
+ display: flex;
+ align-items: center;
+ gap: space(2);
+
+ img {
+ flex-shrink: 0;
+ width: space(5);
+ height: space(5);
+ }
+ }
+
.shortcuts {
dt {
display: block;
diff --git a/config/locales/de.yml b/config/locales/de.yml
index bae7e9e8b8..5ac8dce4f2 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -1512,7 +1512,11 @@ de:
Kapitel einschließlich ALLER enthaltener Seiten wirklich löschen?
Dieser Schritt kann nicht rückgängig gemacht werden.
+ destroy_menu_item:
+ confirm_destroy: Wirklich löschen?
+ destroy: Löschen
edit_configuration:
+ actions: Aktionen
back: "Zurück"
confirm_destroy: Wirklich löschen?
destroy: Löschen
@@ -2115,12 +2119,6 @@ de:
next: ">>"
previous: !!str '<<'
truncate: "..."
- pageflow_scrolled:
- editor:
- section_item:
- set_cutoff: "Paywall Grenze oberhalb setzen"
- reset_cutoff: "Paywall Grenze entfernen"
- cutoff: "Paywall Grenze"
activemodel:
attributes:
pageflow/site_root_entry_form:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9d6a46a1bb..85e3d2d473 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1505,7 +1505,11 @@ en:
Really delete this chapter including ALL its pages?
This operation cannot be undone.
+ destroy_menu_item:
+ confirm_destroy: Really delete this record? This action cannot be undone.
+ destroy: Delete
edit_configuration:
+ actions: Actions
back: "Back"
confirm_destroy: Really delete this record? This action cannot be undone.
destroy: Delete
@@ -2106,12 +2110,6 @@ en:
next: ">>"
previous: !!str '<<'
truncate: "..."
- pageflow_scrolled:
- editor:
- section_item:
- set_cutoff: "Set paywall cutoff above"
- reset_cutoff: "Remove paywall cutoff"
- cutoff: "Paywall cutoff"
activemodel:
attributes:
pageflow/site_root_entry_form:
diff --git a/entry_types/scrolled/config/locales/de.yml b/entry_types/scrolled/config/locales/de.yml
index 10064a6edf..580a534809 100644
--- a/entry_types/scrolled/config/locales/de.yml
+++ b/entry_types/scrolled/config/locales/de.yml
@@ -445,9 +445,9 @@ de:
Karten mit einer Flip-Animation drehen und weitere
Inhalte auf der Rückseite der Karte anbieten.
back: Zurück
- destroy: Löschen
+ confirm_destroy: Soll der Teaser wirklich gelöscht werden?
+ destroy: Teaser löschen
description: Liste von Kacheln mit Bild und Beschriftung
- confirm_delete_item: Soll der Teaser wirklich gelöscht werden?
items: Einträge
name: Teaser-Liste
tabs:
@@ -591,8 +591,8 @@ de:
general: Bildergalerie
edit_item:
back: Zurück
- destroy: Löschen
- confirm_delete_link: Soll der Bildergalerie-Eintrag wirklich gelöscht werden?
+ confirm_destroy: Soll der Bildergalerie-Eintrag wirklich gelöscht werden?
+ destroy: Eintrag löschen
attributes:
image:
label: Bild
@@ -893,8 +893,8 @@ de:
hotspots:
edit_area:
back: Zurück
- destroy: Löschen
- confirm_delete_link: Soll der Hotspot-Bereich wirklich gelöscht werden?
+ confirm_destroy: Soll der Hotspot-Bereich wirklich gelöscht werden?
+ destroy: Hotspot-Bereich löschen
tabs:
area: Hotspot-Bereich
portrait: Hochkant
@@ -1133,10 +1133,6 @@ de:
inline_help: |-
Kapitel kann als Link-Ziel verwendet werden, erscheint
allerdings nicht in der Navigationsleiste.
- confirm_destroy: |-
- Kapitel einschließlich ALLER enthaltener Abschnitte wirklich löschen?
-
- Dieser Schritt kann nicht rückgängig gemacht werden.
save_error: Beim Speichern des Kapitels ist ein Fehler aufgetreten.
tabs:
chapter: Kapitel
@@ -1149,7 +1145,27 @@ de:
hint: Ziehe ein Rechteck, um den wichtigsten Bereich des Bildes zu markieren.
reset: Zurücksetzen
save: Speichern
+ chapter_menu_items:
+ move_to_main: In Kapitel umwandeln
+ move_to_excursions: In Exkurs umwandeln
+ destroy_chapter_menu_item:
+ confirm_destroy: |-
+ Kapitel inklusive ALLER Abschnitte wirklich löschen?
+
+ Diese Aktion kann nicht rückgängig gemacht werden.
+ destroy: Kapitel löschen
+ destroy_content_element_menu_item:
+ confirm_destroy: Element wirklich löschen?
+ destroy: Element löschen
+ selection_label: Auswahl löschen
+ duplicate_content_element_menu_item:
+ label: Element duplizieren
+ selection_label: Auswahl duplizieren
+ destroy_section_menu_item:
+ confirm_destroy: Abschnitt inklusive aller Elemente wirklich löschen?
+ destroy: Abschnitt löschen
edit_section:
+ hidden_info: Dieser Abschnitt ist außerhalb des Editors ausgeblendet.
motif_area_info_text: Markiere den wichtigsten Teil des Hintergrunds, der beim Erreichen des Abschnitts sichtbar und nicht von anderen Elementen überdeckt sein soll.
attributes:
appearance:
@@ -1265,11 +1281,9 @@ de:
header: Element einfügen
no_options: Keine Optionen verfügbar
section_item:
+ cutoff: Paywall Grenze
drag_hint: Ziehen, um den Abschnitt zu verschieben
- duplicate: Duplizieren
- insert_section_above: Abschnitt oberhalb einfügen
- insert_section_below: Abschnitt unterhalb einfügen
- copy_permalink: Permalink kopieren
+ hidden: Nur im Editor sichtbar
save_error: Beim Speichern des Abschnitts ist ein Fehler aufgetreten.
transitions:
beforeAfter: Statische Hintergründe
@@ -1278,9 +1292,15 @@ de:
reveal: Freilegen
scroll: Aus-/Einscrollen
scrollOver: Überlagern
+ section_menu_items:
+ copy_permalink: Permalink kopieren
+ duplicate: Abschnitt duplizieren
hide: Außerhalb des Editors ausblenden
+ insert_section_above: Abschnitt oberhalb einfügen
+ insert_section_below: Abschnitt unterhalb einfügen
+ reset_cutoff: Paywall Grenze entfernen
+ set_cutoff: Paywall Grenze oberhalb setzen
show: Außerhalb des Editors einblenden
- hidden: Nur im Editor sichtbar
section_padding_visualization:
intersecting_auto: "Darstellung des dynamischen Abstands, der sich an die Größe des Motivbereichs anpasst, um Überlappungen von Text und Motiv zu vermeiden"
intersecting_manual: "Darstellung des manuell definierten Abstands, der bei Änderung der Fenstergröße konstant bleibt"
diff --git a/entry_types/scrolled/config/locales/en.yml b/entry_types/scrolled/config/locales/en.yml
index ff5360e890..dc5dba9587 100644
--- a/entry_types/scrolled/config/locales/en.yml
+++ b/entry_types/scrolled/config/locales/en.yml
@@ -436,9 +436,9 @@ en:
Turn cards with a flip animation and offer
additional content on the backside of the card.
back: Back
- destroy: Delete
+ confirm_destroy: Are you sure you want to delete this teaser?
+ destroy: Delete teaser
description: A list of tiles with thumbnail and caption
- confirm_delete_item: Are you sure you want to delete this teaser?
items: Items
name: Teasers
tabs:
@@ -577,8 +577,8 @@ en:
general: Image Gallery
edit_item:
back: Back
- destroy: Delete
- confirm_delete_link: Are you sure you want to delete this image gallery item?
+ confirm_destroy: Are you sure you want to delete this image gallery item?
+ destroy: Delete item
attributes:
image:
label: Image
@@ -878,8 +878,8 @@ en:
hotspots:
edit_area:
back: Back
- destroy: Delete
- confirm_delete_link: Are you sure you want to delete this area?
+ confirm_destroy: Are you sure you want to delete this area?
+ destroy: Delete area
tabs:
area: Hotspot Area
portrait: Portrait
@@ -1117,10 +1117,6 @@ en:
inline_help: |-
Chapter can be used as link destination but does not
appear in the navigation bar.
- confirm_destroy: |-
- Really delete this chapter including ALL its sections?
-
- This operation cannot be undone.
save_error: There was an error while saving this chapter.
tabs:
chapter: Chapter
@@ -1133,7 +1129,27 @@ en:
hint: Drag to select the most important part of the image.
reset: Reset
save: Save
+ chapter_menu_items:
+ move_to_main: Turn into chapter
+ move_to_excursions: Turn into excursion
+ destroy_chapter_menu_item:
+ confirm_destroy: |-
+ Really delete this chapter including ALL its sections?
+
+ This operation cannot be undone.
+ destroy: Delete chapter
+ destroy_content_element_menu_item:
+ confirm_destroy: Really delete this element?
+ destroy: Delete element
+ selection_label: Delete selection
+ duplicate_content_element_menu_item:
+ label: Duplicate element
+ selection_label: Duplicate selection
+ destroy_section_menu_item:
+ confirm_destroy: Really delete this section including all its elements?
+ destroy: Delete section
edit_section:
+ hidden_info: This section is hidden outside of the editor.
motif_area_info_text: Mark the most important part of the backdrop that should be visible and unobstructed when reaching the section.
attributes:
appearance:
@@ -1249,11 +1265,9 @@ en:
header: Insert element
no_options: No options available
section_item:
+ cutoff: Paywall cutoff
drag_hint: Drag to move section
- duplicate: Duplicate
- insert_section_above: Insert section above
- insert_section_below: Insert section below
- copy_permalink: Copy permalink
+ hidden: Only visible in editor
save_error: There was an error while saving this section.
transitions:
beforeAfter: Static Backgrounds
@@ -1262,9 +1276,15 @@ en:
reveal: Reveal
scroll: Scroll
scrollOver: Scroll over
+ section_menu_items:
+ copy_permalink: Copy permalink
+ duplicate: Duplicate section
hide: Hide outside of the editor
+ insert_section_above: Insert section above
+ insert_section_below: Insert section below
+ reset_cutoff: Remove paywall cutoff
+ set_cutoff: Set paywall cutoff above
show: Show outside of the editor
- hidden: Only visible in editor
section_padding_visualization:
intersecting_auto: "Visualization of dynamic padding that adjusts to the motif area size to prevent text from overlapping the motif"
intersecting_manual: "Visualization of manually defined padding that stays constant as viewport size changes"
diff --git a/entry_types/scrolled/package/spec/contentElements/externalLinkList/editor/SidebarEditLinkView-spec.js b/entry_types/scrolled/package/spec/contentElements/externalLinkList/editor/SidebarEditLinkView-spec.js
new file mode 100644
index 0000000000..443b27b9c3
--- /dev/null
+++ b/entry_types/scrolled/package/spec/contentElements/externalLinkList/editor/SidebarEditLinkView-spec.js
@@ -0,0 +1,80 @@
+import {SidebarEditLinkView} from 'contentElements/externalLinkList/editor/SidebarEditLinkView';
+import {ExternalLinkCollection} from 'contentElements/externalLinkList/editor/models/ExternalLinkCollection';
+
+import {editor} from 'pageflow/editor';
+import {DropDownButton} from 'pageflow/testHelpers';
+import {useEditorGlobals, useFakeXhr} from 'support';
+
+describe('SidebarEditLinkView', () => {
+ useFakeXhr();
+ const {createEntry} = useEditorGlobals();
+
+ beforeEach(() => {
+ editor.router = {navigate: jest.fn()};
+ });
+
+ describe('destroy action', () => {
+ it('removes model from collection when confirmed', () => {
+ const entry = createEntry({
+ contentElements: [
+ {
+ id: 1,
+ typeName: 'externalLinkList',
+ configuration: {
+ links: [{id: 1}, {id: 2}]
+ }
+ }
+ ]
+ });
+ const contentElement = entry.contentElements.get(1);
+ const links = ExternalLinkCollection.forContentElement(contentElement, entry);
+ const view = new SidebarEditLinkView({
+ model: links.get(1),
+ collection: links,
+ entry,
+ contentElement
+ });
+ window.confirm = jest.fn(() => true);
+
+ view.render();
+ DropDownButton.find(view).selectMenuItemByName('destroy');
+
+ expect(links.length).toBe(1);
+ expect(links.get(1)).toBeUndefined();
+ });
+
+ });
+
+ describe('goBack', () => {
+ it('posts command to deselect item', () => {
+ const entry = createEntry({
+ contentElements: [
+ {
+ id: 1,
+ typeName: 'externalLinkList',
+ configuration: {
+ links: [{id: 1}]
+ }
+ }
+ ]
+ });
+ const contentElement = entry.contentElements.get(1);
+ const links = ExternalLinkCollection.forContentElement(contentElement, entry);
+ const view = new SidebarEditLinkView({
+ model: links.get(1),
+ collection: links,
+ entry,
+ contentElement
+ });
+ contentElement.postCommand = jest.fn();
+
+ view.render();
+ view.goBack();
+
+ expect(contentElement.postCommand).toHaveBeenCalledWith({
+ type: 'SET_SELECTED_ITEM',
+ index: -1
+ });
+ });
+ });
+});
diff --git a/entry_types/scrolled/package/spec/contentElements/hotspots/editor/SidebarEditAreaView-spec.js b/entry_types/scrolled/package/spec/contentElements/hotspots/editor/SidebarEditAreaView-spec.js
index 3599753422..6f8adfc883 100644
--- a/entry_types/scrolled/package/spec/contentElements/hotspots/editor/SidebarEditAreaView-spec.js
+++ b/entry_types/scrolled/package/spec/contentElements/hotspots/editor/SidebarEditAreaView-spec.js
@@ -1,7 +1,8 @@
import {SidebarEditAreaView} from 'contentElements/hotspots/editor/SidebarEditAreaView';
import {AreasCollection} from 'contentElements/hotspots/editor/models/AreasCollection';
-import {ConfigurationEditor, Tabs, renderBackboneView as render, useFakeTranslations} from 'pageflow/testHelpers';
+import {editor} from 'pageflow/editor';
+import {ConfigurationEditor, DropDownButton, Tabs, renderBackboneView as render, useFakeTranslations} from 'pageflow/testHelpers';
import {useEditorGlobals, useFakeXhr} from 'support';
import userEvent from '@testing-library/user-event';
@@ -9,11 +10,83 @@ describe('SidebarEditAreaView', () => {
useFakeXhr();
const {createEntry} = useEditorGlobals();
+ beforeEach(() => {
+ editor.router = {navigate: jest.fn()};
+ });
+
useFakeTranslations({
'pageflow_scrolled.editor.content_elements.hotspots.edit_area.tabs.area': 'Area',
'pageflow_scrolled.editor.content_elements.hotspots.edit_area.tabs.portrait': 'Portrait'
});
+ describe('destroy action', () => {
+ it('removes model from collection when confirmed', () => {
+ const entry = createEntry({
+ imageFiles: [{perma_id: 10}],
+ contentElements: [
+ {
+ id: 1,
+ typeName: 'hotspots',
+ configuration: {
+ image: 10,
+ areas: [{id: 1}, {id: 2}]
+ }
+ }
+ ]
+ });
+ const contentElement = entry.contentElements.get(1);
+ const areas = AreasCollection.forContentElement(contentElement);
+ const view = new SidebarEditAreaView({
+ model: areas.get(1),
+ collection: areas,
+ entry,
+ contentElement
+ });
+ window.confirm = jest.fn(() => true);
+
+ view.render();
+ DropDownButton.find(view).selectMenuItemByName('destroy');
+
+ expect(areas.length).toBe(1);
+ expect(areas.get(1)).toBeUndefined();
+ });
+ });
+
+ describe('goBack', () => {
+ it('posts command to deselect area', () => {
+ const entry = createEntry({
+ imageFiles: [{perma_id: 10}],
+ contentElements: [
+ {
+ id: 1,
+ typeName: 'hotspots',
+ configuration: {
+ image: 10,
+ areas: [{id: 1}]
+ }
+ }
+ ]
+ });
+ const contentElement = entry.contentElements.get(1);
+ const areas = AreasCollection.forContentElement(contentElement);
+ const view = new SidebarEditAreaView({
+ model: areas.get(1),
+ collection: areas,
+ entry,
+ contentElement
+ });
+ contentElement.postCommand = jest.fn();
+
+ view.render();
+ view.goBack();
+
+ expect(contentElement.postCommand).toHaveBeenCalledWith({
+ type: 'SET_ACTIVE_AREA',
+ index: -1
+ });
+ });
+ });
+
it('renders portrait tab if portrait image is present', () => {
const entry = createEntry({
imageFiles: [
diff --git a/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js b/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js
index c1a338e043..332170cc43 100644
--- a/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js
+++ b/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js
@@ -301,4 +301,49 @@ describe('Chapter', () => {
expect(chapter.isExcursion()).toBe(true);
});
});
+
+ describe('#toggleExcursion', () => {
+ useFakeXhr(() => testContext);
+
+ beforeEach(() => {
+ testContext.entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ storylines: [
+ {id: 100, configuration: {main: true}},
+ {id: 200}
+ ],
+ chapters: [
+ {id: 1, storylineId: 100, position: 0},
+ {id: 2, storylineId: 200, position: 0}
+ ]
+ })
+ });
+ });
+
+ setupGlobals({
+ entry: () => testContext.entry
+ });
+
+ it('moves chapter from main to excursions', () => {
+ const {entry} = testContext;
+ const chapter = entry.chapters.get(1);
+
+ chapter.toggleExcursion();
+
+ expect(chapter.get('storylineId')).toEqual(200);
+ expect(entry.storylines.main().chapters.length).toEqual(0);
+ expect(entry.storylines.excursions().chapters.length).toEqual(2);
+ });
+
+ it('moves chapter from excursions to main', () => {
+ const {entry} = testContext;
+ const chapter = entry.chapters.get(2);
+
+ chapter.toggleExcursion();
+
+ expect(chapter.get('storylineId')).toEqual(100);
+ expect(entry.storylines.main().chapters.length).toEqual(2);
+ expect(entry.storylines.excursions().chapters.length).toEqual(0);
+ });
+ });
});
diff --git a/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/deleteContentElement-spec.js b/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/deleteContentElement-spec.js
index 4855ce46a0..f669389779 100644
--- a/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/deleteContentElement-spec.js
+++ b/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/deleteContentElement-spec.js
@@ -37,7 +37,7 @@ describe('ScrolledEntry', () => {
it('sends item with delete flag to batch endpoint', () => {
const {entry, requests} = testContext;
- entry.deleteContentElement(5);
+ entry.deleteContentElement(entry.contentElements.get(5));
expect(requests[0].url).toBe('/editor/entries/100/scrolled/sections/10/content_elements/batch');
expect(JSON.parse(requests[0].requestBody)).toMatchObject({
@@ -113,7 +113,7 @@ describe('ScrolledEntry', () => {
it('merges the two adjacent content elements ', () => {
const {entry, requests} = testContext;
- entry.deleteContentElement(5);
+ entry.deleteContentElement(entry.contentElements.get(5));
expect(requests[0].url).toBe('/editor/entries/100/scrolled/sections/10/content_elements/batch');
expect(JSON.parse(requests[0].requestBody)).toEqual({
@@ -192,7 +192,7 @@ describe('ScrolledEntry', () => {
it('leaves adjacent content elements unchanged', () => {
const {entry, requests} = testContext;
- entry.deleteContentElement(5);
+ entry.deleteContentElement(entry.contentElements.get(5));
expect(requests[0].url).toBe('/editor/entries/100/scrolled/sections/10/content_elements/batch');
expect(JSON.parse(requests[0].requestBody)).toEqual({
@@ -245,7 +245,7 @@ describe('ScrolledEntry', () => {
it('leaves adjacent content element unchanged', () => {
const {entry, requests} = testContext;
- entry.deleteContentElement(5);
+ entry.deleteContentElement(entry.contentElements.get(5));
expect(requests[0].url).toBe('/editor/entries/100/scrolled/sections/10/content_elements/batch');
expect(JSON.parse(requests[0].requestBody)).toEqual({
diff --git a/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/duplicateContentElement-spec.js b/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/duplicateContentElement-spec.js
new file mode 100644
index 0000000000..2d04a526d4
--- /dev/null
+++ b/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/duplicateContentElement-spec.js
@@ -0,0 +1,96 @@
+import {ScrolledEntry} from 'editor/models/ScrolledEntry';
+import {factories, setupGlobals} from 'pageflow/testHelpers';
+import {useFakeXhr, normalizeSeed} from 'support';
+
+describe('ScrolledEntry', () => {
+ let testContext;
+
+ beforeEach(() => {
+ testContext = {};
+ });
+
+ describe('#duplicateContentElement', () => {
+ beforeEach(() => {
+ testContext.entry = factories.entry(ScrolledEntry, {id: 1}, {
+ entryTypeSeed: normalizeSeed({
+ chapters: [{id: 10}],
+ sections: [{id: 100, chapterId: 10}],
+ contentElements: [
+ {id: 1000, permaId: 1, sectionId: 100, position: 0, typeName: 'textBlock',
+ configuration: {value: 'Some text'}},
+ {id: 1001, permaId: 2, sectionId: 100, position: 1, typeName: 'inlineImage'}
+ ]
+ })
+ });
+ });
+
+ setupGlobals({
+ entry: () => testContext.entry
+ });
+
+ useFakeXhr(() => testContext);
+
+ it('creates content element with same type and configuration', () => {
+ const {entry} = testContext;
+ const contentElement = entry.contentElements.first();
+
+ const newContentElement = entry.duplicateContentElement(contentElement);
+
+ expect(newContentElement.get('typeName')).toBe('textBlock');
+ expect(newContentElement.configuration.get('value')).toBe('Some text');
+ });
+
+ it('posts to batch endpoint', () => {
+ const {entry, requests} = testContext;
+ const contentElement = entry.contentElements.first();
+
+ entry.duplicateContentElement(contentElement);
+
+ expect(requests[0].url).toBe('/editor/entries/1/scrolled/sections/100/content_elements/batch');
+ });
+
+ it('adds duplicated content element after original on server response', () => {
+ const {entry, server} = testContext;
+ const section = entry.sections.first();
+ const contentElement = entry.contentElements.first();
+
+ entry.duplicateContentElement(contentElement);
+
+ server.respondWith(
+ 'PUT',
+ /content_elements\/batch/,
+ [200, {'Content-Type': 'application/json'}, JSON.stringify([
+ {id: 1000},
+ {id: 1002, permaId: 3},
+ {id: 1001}
+ ])]
+ );
+ server.respond();
+
+ expect(section.contentElements.pluck('position')).toEqual([0, 1, 2]);
+ expect(section.contentElements.pluck('id')).toEqual([1000, 1002, 1001]);
+ });
+
+ it('selects duplicated content element after sync', () => {
+ const {entry, server} = testContext;
+ const contentElement = entry.contentElements.first();
+ const listener = jest.fn();
+ entry.on('selectContentElement', listener);
+
+ const newContentElement = entry.duplicateContentElement(contentElement);
+
+ server.respondWith(
+ 'PUT',
+ /content_elements\/batch/,
+ [200, {'Content-Type': 'application/json'}, JSON.stringify([
+ {id: 1000},
+ {id: 1002, permaId: 3},
+ {id: 1001}
+ ])]
+ );
+ server.respond();
+
+ expect(listener).toHaveBeenCalledWith(newContentElement);
+ });
+ });
+});
diff --git a/entry_types/scrolled/package/spec/editor/models/Storyline-spec.js b/entry_types/scrolled/package/spec/editor/models/Storyline-spec.js
index 2ebdb253c2..9a975086ab 100644
--- a/entry_types/scrolled/package/spec/editor/models/Storyline-spec.js
+++ b/entry_types/scrolled/package/spec/editor/models/Storyline-spec.js
@@ -1,7 +1,7 @@
import 'pageflow-scrolled/editor';
import {ScrolledEntry} from 'editor/models/ScrolledEntry';
import {factories, setupGlobals} from 'pageflow/testHelpers';
-import {normalizeSeed} from 'support';
+import {normalizeSeed, useFakeXhr} from 'support';
describe('Storyline', () => {
let testContext;
@@ -34,4 +34,54 @@ describe('Storyline', () => {
expect(chapter.get('position')).toEqual(6);
});
});
+
+ describe('#appendChapter', () => {
+ useFakeXhr(() => testContext);
+
+ beforeEach(() => {
+ testContext.entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ storylines: [{id: 10}, {id: 20}],
+ chapters: [
+ {id: 1, storylineId: 10, position: 0},
+ {id: 2, storylineId: 10, position: 1},
+ {id: 3, storylineId: 20, position: 0}
+ ]
+ })
+ });
+ });
+
+ setupGlobals({
+ entry: () => testContext.entry
+ });
+
+ it('moves chapter to end of storyline', () => {
+ const {entry} = testContext;
+ const sourceStoryline = entry.storylines.get(10);
+ const targetStoryline = entry.storylines.get(20);
+ const chapter = entry.chapters.get(1);
+
+ targetStoryline.appendChapter(chapter);
+
+ expect(sourceStoryline.chapters.length).toEqual(1);
+ expect(targetStoryline.chapters.length).toEqual(2);
+ expect(chapter.get('position')).toEqual(1);
+ });
+
+ it('sets position to 0 for empty storyline', () => {
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ storylines: [{id: 10}, {id: 20}],
+ chapters: [
+ {id: 1, storylineId: 10, position: 0}
+ ]
+ })
+ });
+ const chapter = entry.chapters.get(1);
+
+ entry.storylines.excursions().appendChapter(chapter);
+
+ expect(chapter.get('position')).toEqual(0);
+ });
+ });
});
diff --git a/entry_types/scrolled/package/spec/editor/models/contentElementMenuItems-spec.js b/entry_types/scrolled/package/spec/editor/models/contentElementMenuItems-spec.js
new file mode 100644
index 0000000000..098a95b17c
--- /dev/null
+++ b/entry_types/scrolled/package/spec/editor/models/contentElementMenuItems-spec.js
@@ -0,0 +1,243 @@
+import {DestroyContentElementMenuItem, DuplicateContentElementMenuItem} from 'editor/models/contentElementMenuItems';
+import {ScrolledEntry} from 'editor/models/ScrolledEntry';
+
+import {useFakeTranslations} from 'pageflow/testHelpers';
+import {factories, normalizeSeed} from 'support';
+
+describe('ContentElementMenuItems', () => {
+ describe('DuplicateContentElementMenuItem', () => {
+ useFakeTranslations({
+ 'pageflow_scrolled.editor.duplicate_content_element_menu_item.label': 'Duplicate element',
+ 'pageflow_scrolled.editor.duplicate_content_element_menu_item.selection_label': 'Duplicate selection'
+ });
+
+ it('has Duplicate element label', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ editor.contentElementTypes.register('textBlock', {});
+
+ const menuItem = new DuplicateContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+
+ expect(menuItem.get('label')).toBe('Duplicate element');
+ });
+
+ it('has Duplicate selection label when handleDuplicate is defined', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ editor.contentElementTypes.register('textBlock', {handleDuplicate() {}});
+
+ const menuItem = new DuplicateContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+
+ expect(menuItem.get('label')).toBe('Duplicate selection');
+ });
+
+ it('calls duplicateContentElement on entry when selected', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ entry.duplicateContentElement = jest.fn();
+ editor.contentElementTypes.register('textBlock', {});
+
+ const menuItem = new DuplicateContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+
+ menuItem.selected();
+
+ expect(entry.duplicateContentElement).toHaveBeenCalledWith(contentElement);
+ });
+
+ it('calls handleDuplicate instead of duplicateContentElement if defined', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ const handleDuplicate = jest.fn();
+ entry.duplicateContentElement = jest.fn();
+ editor.contentElementTypes.register('textBlock', {handleDuplicate});
+
+ const menuItem = new DuplicateContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+
+ menuItem.selected();
+
+ expect(handleDuplicate).toHaveBeenCalledWith(contentElement);
+ expect(entry.duplicateContentElement).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('DestroyContentElementMenuItem', () => {
+ useFakeTranslations({
+ 'pageflow_scrolled.editor.destroy_content_element_menu_item.destroy': 'Delete element',
+ 'pageflow_scrolled.editor.destroy_content_element_menu_item.selection_label': 'Delete selection',
+ 'pageflow_scrolled.editor.destroy_content_element_menu_item.confirm_destroy': 'Really delete this element?'
+ });
+
+ it('has Delete element label', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ editor.contentElementTypes.register('textBlock', {});
+
+ const menuItem = new DestroyContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+
+ expect(menuItem.get('label')).toBe('Delete element');
+ });
+
+ it('has Delete selection label when handleDestroy is defined', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ editor.contentElementTypes.register('textBlock', {handleDestroy() {}});
+
+ const menuItem = new DestroyContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+
+ expect(menuItem.get('label')).toBe('Delete selection');
+ });
+
+ it('calls deleteContentElement on entry when confirmed', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ entry.deleteContentElement = jest.fn();
+ editor.contentElementTypes.register('textBlock', {});
+
+ const menuItem = new DestroyContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+ window.confirm = jest.fn().mockReturnValue(true);
+
+ menuItem.selected();
+
+ expect(window.confirm).toHaveBeenCalledWith('Really delete this element?');
+ expect(entry.deleteContentElement).toHaveBeenCalledWith(contentElement);
+ });
+
+ it('calls handleDestroy if content element type defines it', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ const handleDestroy = jest.fn();
+ entry.deleteContentElement = jest.fn();
+ editor.contentElementTypes.register('textBlock', {handleDestroy});
+
+ const menuItem = new DestroyContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+ window.confirm = jest.fn().mockReturnValue(true);
+
+ menuItem.selected();
+
+ expect(handleDestroy).toHaveBeenCalledWith(contentElement);
+ expect(entry.deleteContentElement).toHaveBeenCalled();
+ });
+
+ it('does not call deleteContentElement if handleDestroy returns false', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ entry.deleteContentElement = jest.fn();
+ editor.contentElementTypes.register('textBlock', {
+ handleDestroy() {
+ return false;
+ }
+ });
+
+ const menuItem = new DestroyContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+ window.confirm = jest.fn().mockReturnValue(true);
+
+ menuItem.selected();
+
+ expect(entry.deleteContentElement).not.toHaveBeenCalled();
+ });
+
+ it('does not delete when cancelled', () => {
+ const editor = factories.editorApi();
+ const entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ contentElements: [{id: 1, typeName: 'textBlock'}]
+ })
+ });
+ const contentElement = entry.contentElements.get(1);
+ entry.deleteContentElement = jest.fn();
+ editor.contentElementTypes.register('textBlock', {});
+
+ const menuItem = new DestroyContentElementMenuItem({}, {
+ contentElement,
+ entry,
+ editor
+ });
+ window.confirm = jest.fn().mockReturnValue(false);
+
+ menuItem.selected();
+
+ expect(entry.deleteContentElement).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/entry_types/scrolled/package/spec/editor/models/sectionMenuItems-spec.js b/entry_types/scrolled/package/spec/editor/models/sectionMenuItems-spec.js
new file mode 100644
index 0000000000..b6cc1285b6
--- /dev/null
+++ b/entry_types/scrolled/package/spec/editor/models/sectionMenuItems-spec.js
@@ -0,0 +1,262 @@
+import {
+ HideShowSectionMenuItem,
+ DuplicateSectionMenuItem,
+ InsertSectionAboveMenuItem,
+ InsertSectionBelowMenuItem,
+ CutoffSectionMenuItem,
+ CopyPermalinkMenuItem,
+ DestroySectionMenuItem
+} from 'editor/models/sectionMenuItems';
+
+import {useFakeTranslations} from 'pageflow/testHelpers';
+import {useEditorGlobals} from 'support';
+
+describe('SectionMenuItems', () => {
+ useFakeTranslations({
+ 'pageflow_scrolled.editor.section_menu_items.hide': 'Hide',
+ 'pageflow_scrolled.editor.section_menu_items.show': 'Show',
+ 'pageflow_scrolled.editor.section_menu_items.duplicate': 'Duplicate',
+ 'pageflow_scrolled.editor.section_menu_items.insert_section_above': 'Insert above',
+ 'pageflow_scrolled.editor.section_menu_items.insert_section_below': 'Insert below',
+ 'pageflow_scrolled.editor.section_menu_items.set_cutoff': 'Set cutoff',
+ 'pageflow_scrolled.editor.section_menu_items.reset_cutoff': 'Reset cutoff',
+ 'pageflow_scrolled.editor.section_menu_items.copy_permalink': 'Copy permalink',
+ 'pageflow_scrolled.editor.destroy_section_menu_item.destroy': 'Delete section',
+ 'pageflow_scrolled.editor.destroy_section_menu_item.confirm_destroy': 'Really delete this section?'
+ });
+
+ const {createEntry} = useEditorGlobals();
+
+ describe('HideShowSectionMenuItem', () => {
+ it('sets hidden configuration to true when selected', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new HideShowSectionMenuItem({}, {section});
+
+ menuItem.selected();
+
+ expect(section.configuration.get('hidden')).toBe(true);
+ });
+
+ it('unsets hidden configuration when already hidden', () => {
+ const entry = createEntry({
+ sections: [{id: 1, configuration: {hidden: true}}]
+ });
+ const section = entry.sections.get(1);
+ const menuItem = new HideShowSectionMenuItem({}, {section});
+
+ menuItem.selected();
+
+ expect(section.configuration.get('hidden')).toBeUndefined();
+ });
+
+ it('has Hide label when section is visible', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new HideShowSectionMenuItem({}, {section});
+
+ expect(menuItem.get('label')).toBe('Hide');
+ });
+
+ it('has Show label when section is hidden', () => {
+ const entry = createEntry({
+ sections: [{id: 1, configuration: {hidden: true}}]
+ });
+ const section = entry.sections.get(1);
+ const menuItem = new HideShowSectionMenuItem({}, {section});
+
+ expect(menuItem.get('label')).toBe('Show');
+ });
+
+ it('updates label when hidden state changes', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new HideShowSectionMenuItem({}, {section});
+
+ section.configuration.set('hidden', true);
+
+ expect(menuItem.get('label')).toBe('Show');
+ });
+ });
+
+ describe('DuplicateSectionMenuItem', () => {
+ it('has Duplicate label', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new DuplicateSectionMenuItem({}, {section});
+
+ expect(menuItem.get('label')).toBe('Duplicate');
+ });
+
+ it('calls duplicateSection on chapter when selected', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ section.chapter.duplicateSection = jest.fn();
+ const menuItem = new DuplicateSectionMenuItem({}, {section});
+
+ menuItem.selected();
+
+ expect(section.chapter.duplicateSection).toHaveBeenCalledWith(section);
+ });
+ });
+
+ describe('InsertSectionAboveMenuItem', () => {
+ it('has Insert above label', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new InsertSectionAboveMenuItem({}, {section});
+
+ expect(menuItem.get('label')).toBe('Insert above');
+ });
+
+ it('calls insertSection with before option when selected', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ section.chapter.insertSection = jest.fn();
+ const menuItem = new InsertSectionAboveMenuItem({}, {section});
+
+ menuItem.selected();
+
+ expect(section.chapter.insertSection).toHaveBeenCalledWith({before: section});
+ });
+ });
+
+ describe('InsertSectionBelowMenuItem', () => {
+ it('has Insert below label', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new InsertSectionBelowMenuItem({}, {section});
+
+ expect(menuItem.get('label')).toBe('Insert below');
+ });
+
+ it('calls insertSection with after option when selected', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ section.chapter.insertSection = jest.fn();
+ const menuItem = new InsertSectionBelowMenuItem({}, {section});
+
+ menuItem.selected();
+
+ expect(section.chapter.insertSection).toHaveBeenCalledWith({after: section});
+ });
+ });
+
+ describe('CutoffSectionMenuItem', () => {
+ it('has Set cutoff label when not at section', () => {
+ const entry = createEntry({
+ site: {cutoff_mode_name: 'subscription_headers'},
+ sections: [{id: 1, permaId: 100}]
+ });
+ const section = entry.sections.get(1);
+ const menuItem = new CutoffSectionMenuItem({}, {
+ section,
+ cutoff: entry.cutoff
+ });
+
+ expect(menuItem.get('label')).toBe('Set cutoff');
+ });
+
+ it('has Reset cutoff label when at section', () => {
+ const entry = createEntry({
+ site: {cutoff_mode_name: 'subscription_headers'},
+ metadata: {configuration: {cutoff_section_perma_id: 100}},
+ sections: [{id: 1, permaId: 100}]
+ });
+ const section = entry.sections.get(1);
+ const menuItem = new CutoffSectionMenuItem({}, {
+ section,
+ cutoff: entry.cutoff
+ });
+
+ expect(menuItem.get('label')).toBe('Reset cutoff');
+ });
+
+ it('sets cutoff to section when selected', () => {
+ const entry = createEntry({
+ site: {cutoff_mode_name: 'subscription_headers'},
+ sections: [{id: 1, permaId: 100}]
+ });
+ const section = entry.sections.get(1);
+ const menuItem = new CutoffSectionMenuItem({}, {
+ section,
+ cutoff: entry.cutoff
+ });
+
+ menuItem.selected();
+
+ expect(entry.metadata.configuration.get('cutoff_section_perma_id')).toBe(100);
+ });
+
+ it('resets cutoff when already at section', () => {
+ const entry = createEntry({
+ site: {cutoff_mode_name: 'subscription_headers'},
+ metadata: {configuration: {cutoff_section_perma_id: 100}},
+ sections: [{id: 1, permaId: 100}]
+ });
+ const section = entry.sections.get(1);
+ const menuItem = new CutoffSectionMenuItem({}, {
+ section,
+ cutoff: entry.cutoff
+ });
+
+ menuItem.selected();
+
+ expect(entry.metadata.configuration.get('cutoff_section_perma_id')).toBeUndefined();
+ });
+ });
+
+ describe('CopyPermalinkMenuItem', () => {
+ it('has Copy permalink label', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new CopyPermalinkMenuItem({}, {entry, section});
+
+ expect(menuItem.get('label')).toBe('Copy permalink');
+ });
+
+ it('supports separated attribute', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new CopyPermalinkMenuItem({separated: true}, {entry, section});
+
+ expect(menuItem.get('separated')).toBe(true);
+ });
+
+ it('copies permalink to clipboard when selected', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ entry.getSectionPermalink = jest.fn().mockReturnValue('http://example.com/section');
+ const section = entry.sections.get(1);
+ const menuItem = new CopyPermalinkMenuItem({}, {entry, section});
+ navigator.clipboard = {writeText: jest.fn()};
+
+ menuItem.selected();
+
+ expect(entry.getSectionPermalink).toHaveBeenCalledWith(section);
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/section');
+ });
+ });
+
+ describe('DestroySectionMenuItem', () => {
+ it('has Delete section label', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ const menuItem = new DestroySectionMenuItem({}, {section});
+
+ expect(menuItem.get('label')).toBe('Delete section');
+ });
+
+ it('calls destroyWithDelay on section when confirmed', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const section = entry.sections.get(1);
+ section.destroyWithDelay = jest.fn();
+ const menuItem = new DestroySectionMenuItem({}, {section});
+ window.confirm = jest.fn().mockReturnValue(true);
+
+ menuItem.selected();
+
+ expect(window.confirm).toHaveBeenCalledWith('Really delete this section?');
+ expect(section.destroyWithDelay).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/entry_types/scrolled/package/spec/editor/views/EditContentElementView-spec.js b/entry_types/scrolled/package/spec/editor/views/EditContentElementView-spec.js
index 62678c069d..137e9c64c5 100644
--- a/entry_types/scrolled/package/spec/editor/views/EditContentElementView-spec.js
+++ b/entry_types/scrolled/package/spec/editor/views/EditContentElementView-spec.js
@@ -65,68 +65,4 @@ describe('EditContentElementView', () => {
contentElement: contentElement
});
});
-
- it('lets content element types override detroy button', () => {
- const editor = factories.editorApi();
- const entry = factories.entry(ScrolledEntry, {}, {
- entryTypeSeed: normalizeSeed({
- contentElements: [
- {id: 1, typeName: 'textBlock'}
- ]
- })
- });
- const contentElement = entry.contentElements.get(1);
- const view = new EditContentElementView({
- model: contentElement,
- editor,
- entry
- });
- const handleDestroy = jest.fn();
- entry.deleteContentElement = jest.fn();
-
- editor.contentElementTypes.register('textBlock', {
- configurationEditor() {
- this.tab('general', function() {});
- },
-
- handleDestroy
- });
- view.render();
-
- view.destroyModel();
-
- expect(handleDestroy).toHaveBeenCalledWith(contentElement);
- });
-
- it('does not call deleteContentElement if handleDestroy returns false', () => {
- const editor = factories.editorApi();
- const entry = factories.entry(ScrolledEntry, {}, {
- entryTypeSeed: normalizeSeed({
- contentElements: [
- {id: 1, typeName: 'textBlock'}
- ]
- })
- });
- const view = new EditContentElementView({
- model: entry.contentElements.get(1),
- editor,
- entry
- });
- entry.deleteContentElement = jest.fn();
-
- editor.contentElementTypes.register('textBlock', {
- configurationEditor() {
- this.tab('general', function() {});
- },
-
- handleDestroy() {
- return false;
- }
- });
- view.render();
-
- view.destroyModel();
-
- expect(entry.deleteContentElement).not.toHaveBeenCalled();
- });
});
diff --git a/entry_types/scrolled/package/spec/editor/views/EditSectionView-spec.js b/entry_types/scrolled/package/spec/editor/views/EditSectionView-spec.js
index d46d5f0f0f..3657860a46 100644
--- a/entry_types/scrolled/package/spec/editor/views/EditSectionView-spec.js
+++ b/entry_types/scrolled/package/spec/editor/views/EditSectionView-spec.js
@@ -1,6 +1,6 @@
import {EditSectionView} from 'editor/views/EditSectionView';
-import {ConfigurationEditor} from 'pageflow/testHelpers';
+import {ConfigurationEditor, DropDownButton, useFakeTranslations} from 'pageflow/testHelpers';
import {useEditorGlobals} from 'support';
describe('EditSectionView', () => {
@@ -254,4 +254,34 @@ describe('EditSectionView', () => {
expect(configurationEditor.visibleInputPropertyNames())
.not.toContain('backdropEffectsMobile');
});
+
+ describe('actions dropdown', () => {
+ useFakeTranslations({
+ 'pageflow_scrolled.editor.section_menu_items.duplicate': 'Duplicate',
+ 'pageflow_scrolled.editor.section_menu_items.insert_section_above': 'Insert above',
+ 'pageflow_scrolled.editor.section_menu_items.insert_section_below': 'Insert below',
+ 'pageflow_scrolled.editor.section_menu_items.hide': 'Hide',
+ 'pageflow_scrolled.editor.section_menu_items.copy_permalink': 'Copy permalink',
+ 'pageflow_scrolled.editor.destroy_section_menu_item.destroy': 'Delete section'
+ });
+
+ it('includes section-specific menu items', () => {
+ const entry = createEntry({sections: [{id: 1}]});
+ const view = new EditSectionView({
+ model: entry.sections.get(1),
+ entry
+ });
+
+ view.render();
+ const allDropDowns = DropDownButton.findAll(view);
+ const actionsDropDown = allDropDowns[0];
+
+ expect(actionsDropDown.menuItemLabels()).toContain('Duplicate');
+ expect(actionsDropDown.menuItemLabels()).toContain('Insert above');
+ expect(actionsDropDown.menuItemLabels()).toContain('Insert below');
+ expect(actionsDropDown.menuItemLabels()).toContain('Hide');
+ expect(actionsDropDown.menuItemLabels()).toContain('Copy permalink');
+ expect(actionsDropDown.menuItemLabels()).toContain('Delete section');
+ });
+ });
});
diff --git a/entry_types/scrolled/package/spec/editor/views/SectionItemView-spec.js b/entry_types/scrolled/package/spec/editor/views/SectionItemView-spec.js
index 751d03b20f..62431f73f3 100644
--- a/entry_types/scrolled/package/spec/editor/views/SectionItemView-spec.js
+++ b/entry_types/scrolled/package/spec/editor/views/SectionItemView-spec.js
@@ -1,7 +1,6 @@
import {SectionItemView} from 'editor/views/SectionItemView';
import {useEditorGlobals, useFakeXhr} from 'support';
-import userEvent from '@testing-library/user-event';
import {useFakeTranslations, renderBackboneView as render} from 'pageflow/testHelpers';
import '@testing-library/jest-dom/extend-expect';
@@ -9,38 +8,32 @@ describe('SectionItemView', () => {
useFakeXhr();
useFakeTranslations({
- 'pageflow_scrolled.editor.section_item.hide': 'Hide',
- 'pageflow_scrolled.editor.section_item.show': 'Show',
- 'pageflow_scrolled.editor.section_item.set_cutoff': 'Set cutoff point',
- 'pageflow_scrolled.editor.section_item.reset_cutoff': 'Remove cutoff point',
+ 'pageflow_scrolled.editor.section_menu_items.hide': 'Hide',
+ 'pageflow_scrolled.editor.section_menu_items.show': 'Show',
+ 'pageflow_scrolled.editor.section_menu_items.set_cutoff': 'Set cutoff point',
+ 'pageflow_scrolled.editor.section_menu_items.reset_cutoff': 'Remove cutoff point',
'pageflow_scrolled.editor.section_item.cutoff': 'Cutoff point',
});
const {createEntry} = useEditorGlobals();
- it('offer menu item to hide and show section', async () => {
+
+ it('renders menu with section menu items', () => {
const entry = createEntry({
sections: [
{id: 1, permaId: 100}
]
});
- const section = entry.sections.get(1)
const view = new SectionItemView({
entry,
- model: section
+ model: entry.sections.get(1)
});
- const user = userEvent.setup();
const {getByRole} = render(view);
- await user.click(getByRole('link', {name: 'Hide'}));
-
- expect(section.configuration.get('hidden')).toEqual(true);
- await user.click(getByRole('link', {name: 'Show'}));
-
- expect(section.configuration.get('hidden')).toBeUndefined();
+ expect(getByRole('link', {name: 'Hide'})).not.toBeNull();
});
- it('does not offer menu item to set cutoff section by default', () => {
+ it('does not render cutoff menu item by default', () => {
const entry = createEntry({
sections: [
{id: 1, permaId: 100}
@@ -56,7 +49,7 @@ describe('SectionItemView', () => {
expect(queryByRole('link', {name: 'Set cutoff point'})).toBeNull();
});
- it('offers menu item to set cutoff section if site has cutoff mode', async () => {
+ it('renders cutoff menu item if site has cutoff mode', () => {
const entry = createEntry({
site: {
cutoff_mode_name: 'subscription_headers'
@@ -70,55 +63,9 @@ describe('SectionItemView', () => {
model: entry.sections.get(1)
});
- const user = userEvent.setup();
- const {getByRole} = render(view);
- await user.click(getByRole('link', {name: 'Set cutoff point'}));
-
- expect(entry.metadata.configuration.get('cutoff_section_perma_id')).toEqual(100);
- });
-
- it('offers menu item to reset cutoff section if site has cutoff mode', async () => {
- const entry = createEntry({
- site: {
- cutoff_mode_name: 'subscription_headers'
- },
- metadata: {configuration: {cutoff_section_perma_id: 101}},
- sections: [
- {id: 1, permaId: 100},
- {id: 2, permaId: 101}
- ]
- });
- const view = new SectionItemView({
- entry,
- model: entry.sections.get(2)
- });
-
- const user = userEvent.setup();
const {getByRole} = render(view);
- await user.click(getByRole('link', {name: 'Remove cutoff point'}));
-
- expect(entry.metadata.configuration.get('cutoff_section_perma_id')).toBeUndefined();
- });
-
- it('updates menu item when cutoff section changes', () => {
- const entry = createEntry({
- site: {
- cutoff_mode_name: 'subscription_headers'
- },
- sections: [
- {id: 1, permaId: 100},
- {id: 2, permaId: 101}
- ]
- });
- const view = new SectionItemView({
- entry,
- model: entry.sections.get(2)
- });
-
- const {queryByRole} = render(view);
- entry.metadata.configuration.set('cutoff_section_perma_id', 101)
- expect(queryByRole('link', {name: 'Remove cutoff point'})).not.toBeNull();
+ expect(getByRole('link', {name: 'Set cutoff point'})).not.toBeNull();
});
it('renders cutoff indicator', () => {
diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/duplicateNodes-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/duplicateNodes-spec.js
new file mode 100644
index 0000000000..46d519101e
--- /dev/null
+++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/duplicateNodes-spec.js
@@ -0,0 +1,285 @@
+/** @jsx jsx */
+import {duplicateNodes} from 'frontend/inlineEditing/EditableText/duplicateNodes';
+
+import {createHyperscript} from 'slate-hyperscript';
+
+const h = createHyperscript({
+ elements: {
+ paragraph: {type: 'paragraph'},
+ heading: {type: 'heading'},
+ blockQuote: {type: 'block-quote'},
+ bulletedList: {type: 'bulleted-list'},
+ listItem: {type: 'list-item'}
+ },
+});
+
+// Strip meta tags to make deep equality checks work
+const jsx = (tagName, attributes, ...children) => {
+ delete attributes.__self;
+ delete attributes.__source;
+ return h(tagName, attributes, ...children);
+}
+
+describe('duplicateNodes', () => {
+ it('duplicates single selected paragraph', () => {
+ const editor = (
+
${t('save_error')}
@@ -74,8 +67,7 @@ export const EditConfigurationView = Marionette.Layout.extend({ serializeData() { return { t: key => this.t(key), - backLabel: this.getBackLabel(), - hideDestroyButton: _.result(this, 'hideDestroyButton') + backLabel: this.getBackLabel() }; }, @@ -86,8 +78,13 @@ export const EditConfigurationView = Marionette.Layout.extend({ }, events: { - 'click a.back': 'goBack', - 'click a.destroy': 'destroy' + 'click a.back': 'goBack' + }, + + destroyEvent: 'destroy', + + initialize() { + this.listenTo(this.model, _.result(this, 'destroyEvent'), this.goBack); }, onRender: function() { @@ -96,28 +93,40 @@ export const EditConfigurationView = Marionette.Layout.extend({ this.configurationEditor = new ConfigurationEditorView({ tabTranslationKeyPrefix: `${translationKeyPrefix}.tabs`, attributeTranslationKeyPrefixes: [`${translationKeyPrefix}.attributes`], - model: this.model.configuration, + model: this.getConfigurationModel(), tab: _.result(this, 'defaultTab') }); this.configure(this.configurationEditor); this.configurationContainer.show(this.configurationEditor); - }, - onShow: function() { - this.configurationEditor.refreshScroller(); + this.renderActionsDropDown(); }, - destroy: function() { - if (window.confirm(this.t('confirm_destroy'))) { - if (this.destroyModel() !== false) { - this.goBack(); - } + renderActionsDropDown() { + const items = new Backbone.Collection(this.getActionsMenuItems()); + + if (!items.length) { + return; } + + this.$el.find('.actions_drop_down_button').append( + this.subview(new DropDownButtonView({ + items, + label: this.t('actions'), + ellipsisIcon: true, + openOnClick: true, + alignMenu: 'right' + })).el + ); + }, + + getActionsMenuItems() { + return []; }, - destroyModel() { - this.model.destroyWithDelay(); + onShow: function() { + this.configurationEditor.refreshScroller(); }, goBack: function() { @@ -125,6 +134,10 @@ export const EditConfigurationView = Marionette.Layout.extend({ editor.navigate(path, {trigger: true}); }, + getConfigurationModel() { + return this.model.configuration; + }, + getBackLabel() { return this.t(_.result(this, 'goBackPath') ? 'back' : 'outline'); }, diff --git a/package/src/editor/views/InfoBoxView.js b/package/src/editor/views/InfoBoxView.js index ddaa5f4b9c..0a5eff9910 100644 --- a/package/src/editor/views/InfoBoxView.js +++ b/package/src/editor/views/InfoBoxView.js @@ -2,23 +2,33 @@ import Marionette from 'backbone.marionette'; import {attributeBinding} from 'pageflow/ui'; -export const InfoBoxView = Marionette.View.extend({ +export const InfoBoxView = Marionette.ItemView.extend({ className: 'info_box', mixins: [attributeBinding], + template: (data) => data.icon ? + `