diff --git a/app/assets/stylesheets/pageflow/editor/drop_down_button.scss b/app/assets/stylesheets/pageflow/editor/drop_down_button.scss
index 12d2424969..0988992395 100644
--- a/app/assets/stylesheets/pageflow/editor/drop_down_button.scss
+++ b/app/assets/stylesheets/pageflow/editor/drop_down_button.scss
@@ -10,6 +10,11 @@
@include icon-only-button;
}
+ > button.ellipsis_icon {
+ @include fa-ellipsis-v-icon;
+ width: 31px;
+ }
+
&.full_width {
width: 100%;
diff --git a/app/assets/stylesheets/pageflow/editor/inputs/file_input.scss b/app/assets/stylesheets/pageflow/editor/inputs/file_input.scss
index a1a9cee9a9..c5f681ddbf 100644
--- a/app/assets/stylesheets/pageflow/editor/inputs/file_input.scss
+++ b/app/assets/stylesheets/pageflow/editor/inputs/file_input.scss
@@ -38,11 +38,6 @@
}
.drop_down_button {
- button {
- @include fa-ellipsis-v-icon;
- width: 31px;
- }
-
position: absolute;
bottom: 0;
left: 70px;
diff --git a/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/sections_controller.rb b/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/sections_controller.rb
index 5372782dd1..5b98ab5f09 100644
--- a/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/sections_controller.rb
+++ b/entry_types/scrolled/app/controllers/pageflow_scrolled/editor/sections_controller.rb
@@ -6,13 +6,21 @@ class SectionsController < ActionController::Base
def create
chapter = Chapter.all_for_revision(@entry.draft).find(params[:chapter_id])
- section = chapter.sections.create(section_params)
+ section = chapter.create_section(section_params)
- render partial: 'pageflow_scrolled/sections/section',
+ render partial: 'pageflow_scrolled/editor/sections/section_with_content_elements',
locals: {section: section},
status: :created
end
+ def duplicate
+ section = Section.all_for_revision(@entry.draft).find(params[:id])
+
+ render partial: 'pageflow_scrolled/editor/sections/section_with_content_elements',
+ locals: {section: section.chapter.duplicate_section(section)},
+ status: :created
+ end
+
def update
section = Section.all_for_revision(@entry.draft).find(params[:id])
section.update(section_params)
diff --git a/entry_types/scrolled/app/models/pageflow_scrolled/chapter.rb b/entry_types/scrolled/app/models/pageflow_scrolled/chapter.rb
index 00354c7b3e..edf8c36c5b 100644
--- a/entry_types/scrolled/app/models/pageflow_scrolled/chapter.rb
+++ b/entry_types/scrolled/app/models/pageflow_scrolled/chapter.rb
@@ -16,6 +16,23 @@ class Chapter < Pageflow::ApplicationRecord
attr_accessor :revision # used on :create to lazily create storyline
before_validation :ensure_storyline, on: :create
+ def create_section(attributes = {})
+ shift_section_positions(from: attributes[:position])
+
+ section = sections.create!(attributes)
+ section.content_elements.create!(type_name: 'textBlock')
+
+ section
+ end
+
+ def duplicate_section(section)
+ shift_section_positions(from: section.position + 1)
+
+ section.duplicate do |new_section|
+ new_section.position = section.position + 1
+ end
+ end
+
def self.all_for_revision(revision)
joins(storyline: :revision)
.where(pageflow_scrolled_storylines: {revision_id: revision})
@@ -23,6 +40,12 @@ def self.all_for_revision(revision)
private
+ def shift_section_positions(from:)
+ sections
+ .where('position >= ?', from)
+ .update_all('position = position + 1')
+ end
+
def ensure_storyline
return if storyline.present?
unless Storyline.all_for_revision(revision).exists?
diff --git a/entry_types/scrolled/app/views/pageflow_scrolled/editor/sections/_section_with_content_elements.json.jbuilder b/entry_types/scrolled/app/views/pageflow_scrolled/editor/sections/_section_with_content_elements.json.jbuilder
new file mode 100644
index 0000000000..5dae12570e
--- /dev/null
+++ b/entry_types/scrolled/app/views/pageflow_scrolled/editor/sections/_section_with_content_elements.json.jbuilder
@@ -0,0 +1,10 @@
+json.key_format!(camelize: :lower)
+
+json.partial! 'pageflow_scrolled/sections/section', section: section
+
+json.content_elements do
+ json.array!(section.content_elements) do |content_element|
+ json.partial! 'pageflow_scrolled/content_elements/content_element',
+ content_element: content_element
+ end
+end
diff --git a/entry_types/scrolled/config/locales/new/duplicate_section.de.yml b/entry_types/scrolled/config/locales/new/duplicate_section.de.yml
new file mode 100644
index 0000000000..a442526228
--- /dev/null
+++ b/entry_types/scrolled/config/locales/new/duplicate_section.de.yml
@@ -0,0 +1,7 @@
+de:
+ pageflow_scrolled:
+ editor:
+ section_item:
+ duplicate: Duplizieren
+ insert_section_above: Abschnitt oberhalb einfügen
+ insert_section_below: Abschnitt unterhalb einfügen
diff --git a/entry_types/scrolled/config/locales/new/duplicate_section.en.yml b/entry_types/scrolled/config/locales/new/duplicate_section.en.yml
new file mode 100644
index 0000000000..6f43cc45c1
--- /dev/null
+++ b/entry_types/scrolled/config/locales/new/duplicate_section.en.yml
@@ -0,0 +1,7 @@
+en:
+ pageflow_scrolled:
+ editor:
+ section_item:
+ duplicate: Duplicate
+ insert_section_above: Insert section above
+ insert_section_below: Insert section below
diff --git a/entry_types/scrolled/config/routes.rb b/entry_types/scrolled/config/routes.rb
index 57d5f81052..2c66db7767 100644
--- a/entry_types/scrolled/config/routes.rb
+++ b/entry_types/scrolled/config/routes.rb
@@ -11,6 +11,10 @@
put :order
end
+ member do
+ post :duplicate
+ end
+
resources :content_elements do
collection do
put :batch
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 226cc1feab..954d52aa2a 100644
--- a/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js
+++ b/entry_types/scrolled/package/spec/editor/models/Chapter-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 {useFakeXhr, normalizeSeed} from 'support';
describe('Chapter', () => {
let testContext;
@@ -32,4 +32,186 @@ describe('Chapter', () => {
expect(section.configuration.get('transition')).toEqual('beforeAfter');
});
});
+
+ describe('#insertSection', () => {
+ beforeEach(() => {
+ testContext.entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ chapters: [{id: 10}],
+ sections: [
+ {id: 100, chapterId: 10, position: 0},
+ {id: 101, chapterId: 10, position: 1}
+ ]
+ })
+ });
+ });
+
+ setupGlobals({
+ entry: () => testContext.entry
+ });
+
+ it('re-indexes pages when inserting before other section', () => {
+ const {entry} = testContext;
+ const chapter = entry.chapters.first();
+
+ chapter.insertSection({before: chapter.sections.last()});
+
+ expect(chapter.sections.pluck('position')).toEqual([0, 1, 2]);
+ expect(chapter.sections.pluck('id')).toEqual([100, undefined, 101]);
+ });
+
+ it('re-indexes pages when inserting after other section', () => {
+ const {entry} = testContext;
+ const chapter = entry.chapters.first();
+
+ chapter.insertSection({after: chapter.sections.first()});
+
+ expect(chapter.sections.pluck('position')).toEqual([0, 1, 2]);
+ expect(chapter.sections.pluck('id')).toEqual([100, undefined, 101]);
+ });
+
+ describe('with sparse positions', () => {
+ beforeEach(() => {
+ testContext.entry = factories.entry(ScrolledEntry, {}, {
+ entryTypeSeed: normalizeSeed({
+ chapters: [{id: 10}],
+ sections: [
+ {id: 100, chapterId: 10, position: 7},
+ {id: 101, chapterId: 10, position: 8}
+ ]
+ })
+ });
+ });
+
+ it('re-indexes pages when inserting after other section', () => {
+ const {entry} = testContext;
+ const chapter = entry.chapters.first();
+
+ chapter.insertSection({after: chapter.sections.first()});
+
+ expect(chapter.sections.pluck('position')).toEqual([7, 8, 9]);
+ expect(chapter.sections.pluck('id')).toEqual([100, undefined, 101]);
+ });
+ });
+ });
+
+ describe('#duplicateSection', () => {
+ beforeEach(() => {
+ testContext.entry = factories.entry(ScrolledEntry, {id: 1}, {
+ entryTypeSeed: normalizeSeed({
+ chapters: [{id: 10}],
+ sections: [
+ {id: 100, chapterId: 10, position: 0, configuration: {transition: 'scroll'}},
+ {id: 101, chapterId: 10, position: 1}
+ ],
+ contentElements: [
+ {id: 1000, permaId: 1, sectionId: 101, position: 0, typeName: 'inlineImage'},
+ {id: 1001, permaId: 2, sectionId: 101, position: 1, typeName: 'textBlock'}
+ ]
+ })
+ });
+ });
+
+ setupGlobals({
+ entry: () => testContext.entry
+ });
+
+ useFakeXhr(() => testContext);
+
+ it('re-indexes pages when inserting before other section', () => {
+ const {entry} = testContext;
+ const chapter = entry.chapters.first();
+
+ chapter.duplicateSection(chapter.sections.first());
+
+ expect(chapter.sections.pluck('position')).toEqual([0, 1, 2]);
+ expect(chapter.sections.pluck('id')).toEqual([100, undefined, 101]);
+ });
+
+ it('posts requests to duplicate action and adds content elements', () => {
+ const {entry, server, requests} = testContext;
+ const chapter = entry.chapters.first();
+
+ chapter.duplicateSection(chapter.sections.first());
+
+ expect(requests[0].method).toBe('POST');
+ expect(requests[0].url).toBe('/editor/entries/1/scrolled/sections/100/duplicate');
+
+ expect(chapter.sections.pluck('position')).toEqual([0, 1, 2])
+ expect(chapter.sections.pluck('id')).toEqual([100, undefined, 101])
+
+ server.respond(
+ 'POST', '/editor/entries/1/scrolled/sections/100/duplicate',
+ [200, {'Content-Type': 'application/json'}, JSON.stringify({
+ id: 102,
+ configuration: {transition: 'scroll'},
+ contentElements: [
+ {
+ id: 1002,
+ permaId: 3,
+ sectionId: 102,
+ position: 0,
+ typeName: 'inlineImage',
+ configuration: {image: 5}
+ },
+ {
+ id: 1003,
+ permaId: 4,
+ sectionId: 102,
+ position: 0,
+ typeName: 'textBlock',
+ configuration: {value: 'Some text'}
+ },
+ ]
+ })]
+ );
+
+ expect(requests.length).toEqual(1);
+ expect(chapter.sections.pluck('id')).toEqual([100, 102, 101]);
+ const section = chapter.sections.get(102);
+
+ expect(section.configuration.get('transition')).toEqual('scroll');
+ expect(section.contentElements.pluck('id')).toEqual([1002, 1003]);
+ expect(section.contentElements.pluck('typeName')).toEqual(['inlineImage', 'textBlock']);
+ expect(section.contentElements.first().configuration.get('image')).toEqual(5);
+ expect(section.contentElements.last().configuration.get('value')).toEqual('Some text');
+ });
+
+ it('does not use duplicate url on subsequent save', () => {
+ const {entry, server, requests} = testContext;
+ const chapter = entry.chapters.first();
+
+ const section = chapter.duplicateSection(chapter.sections.first());
+
+ server.respond(
+ 'POST', '/editor/entries/1/scrolled/sections/100/duplicate',
+ [200, {'Content-Type': 'application/json'}, JSON.stringify({
+ id: 102,
+ configuration: {transition: 'scroll'},
+ contentElements: [
+ {
+ id: 1002,
+ permaId: 3,
+ sectionId: 102,
+ position: 0,
+ typeName: 'inlineImage',
+ configuration: {image: 5}
+ },
+ {
+ id: 1003,
+ permaId: 4,
+ sectionId: 102,
+ position: 0,
+ typeName: 'textBlock',
+ configuration: {value: 'Some text'}
+ },
+ ]
+ })]
+ );
+ section.save();
+
+ expect(requests.length).toEqual(2);
+ expect(requests[1].url).toBe('/editor/entries/1/scrolled/sections/102');
+ });
+ });
});
diff --git a/entry_types/scrolled/package/src/editor/models/Chapter.js b/entry_types/scrolled/package/src/editor/models/Chapter.js
index be5fe65bdb..677890adce 100644
--- a/entry_types/scrolled/package/src/editor/models/Chapter.js
+++ b/entry_types/scrolled/package/src/editor/models/Chapter.js
@@ -8,6 +8,8 @@ import {
ForeignKeySubsetCollection
} from 'pageflow/editor';
+import {Section} from './Section';
+
export const Chapter = Backbone.Model.extend({
mixins: [
configurationContainer({
@@ -30,29 +32,57 @@ export const Chapter = Backbone.Model.extend({
this.entry = options.entry;
},
- addSection(attributes) {
- const section = this.sections.create({
- position: this.sections.length,
- chapterId: this.id,
- configuration: {
- transition: this.entry.metadata.configuration.get('defaultTransition')
- },
- ...attributes
- }, {
- contentElements: this.entry.contentElements
- });
+ addSection(attributes, {select, ...options} = {}) {
+ const section = this.sections.create(
+ new Section(
+ {
+ position: this.sections.length,
+ chapterId: this.id,
+ configuration: {
+ transition: this.entry.metadata.configuration.get('defaultTransition')
+ },
+ ...attributes
+ },
+ {
+ contentElements: this.entry.contentElements
+ }
+ ),
+ options
+ );
- section.once('sync', () => {
- this.entry.trigger('selectSectionSettings', section);
+ section.once('sync', (model, response) => {
+ if (select) {
+ this.entry.trigger('selectSectionSettings', section);
+ }
this.entry.trigger('scrollToSection', section);
- section.contentElements.create({
- typeName: 'textBlock',
- configuration: {
- }
- });
+ section.configuration.set(response.configuration, {autoSave: false});
+ section.contentElements.add(response.contentElements);
});
return section;
+ },
+
+ insertSection({before, after}, options) {
+ const position = before ? before.get('position') : after.get('position') + 1;
+
+ this.sections.each((section) => {
+ if (section.get('position') >= position) {
+ section.set('position', section.get('position') + 1);
+ }
+ });
+
+ const newSection = this.addSection({position}, options);
+
+ this.sections.sort();
+ return newSection;
+ },
+
+ duplicateSection(section) {
+ const newSection = this.insertSection({after: section}, {
+ url: `${section.url()}/duplicate`,
+ });
+
+ return newSection;
}
});
diff --git a/entry_types/scrolled/package/src/editor/views/ChapterItemView.js b/entry_types/scrolled/package/src/editor/views/ChapterItemView.js
index 794a74e845..71c7ec1718 100644
--- a/entry_types/scrolled/package/src/editor/views/ChapterItemView.js
+++ b/entry_types/scrolled/package/src/editor/views/ChapterItemView.js
@@ -35,7 +35,7 @@ export const ChapterItemView = Marionette.Layout.extend({
events: cssModulesUtils.events(styles, {
'click addSection': function() {
- this.model.addSection();
+ this.model.addSection({}, {select: true});
},
'click link': function() {
diff --git a/entry_types/scrolled/package/src/editor/views/SectionItemView.js b/entry_types/scrolled/package/src/editor/views/SectionItemView.js
index 1832d2cd63..aaff20d1a2 100644
--- a/entry_types/scrolled/package/src/editor/views/SectionItemView.js
+++ b/entry_types/scrolled/package/src/editor/views/SectionItemView.js
@@ -1,6 +1,7 @@
import I18n from 'i18n-js';
+import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
-import {modelLifecycleTrackingView} from 'pageflow/editor';
+import {modelLifecycleTrackingView, DropDownButtonView} from 'pageflow/editor';
import {cssModulesUtils} from 'pageflow/ui';
import {SectionThumbnailView} from './SectionThumbnailView'
@@ -15,6 +16,7 @@ export const SectionItemView = Marionette.ItemView.extend({
template: (data) => `
+
`,
- ui: cssModulesUtils.ui(styles, 'thumbnail'),
+ ui: cssModulesUtils.ui(styles, 'thumbnail', 'dropDownButton'),
events: {
[`click .${styles.clickMask}`]: function() {
@@ -55,7 +57,10 @@ export const SectionItemView = Marionette.ItemView.extend({
},
onRender() {
- this.updateActive();
+ if (this.updateActive()) {
+ setTimeout(() => this.$el[0].scrollIntoView({block: 'nearest'}), 10)
+ }
+
this.$el.toggleClass(styles.invert, !!this.model.configuration.get('invert'));
this.subview(new SectionThumbnailView({
@@ -63,6 +68,36 @@ export const SectionItemView = Marionette.ItemView.extend({
model: this.model,
entry: this.options.entry
}));
+
+ const dropDownMenuItems = new Backbone.Collection();
+
+ dropDownMenuItems.add(new MenuItem({
+ label: I18n.t('pageflow_scrolled.editor.section_item.duplicate')
+ }, {
+ selected: () =>
+ this.model.chapter.duplicateSection(this.model)
+ }));
+
+ dropDownMenuItems.add(new MenuItem({
+ label: I18n.t('pageflow_scrolled.editor.section_item.insert_section_above')
+ }, {
+ selected: () =>
+ this.model.chapter.insertSection({before: this.model})
+ }));
+
+ dropDownMenuItems.add(new MenuItem({
+ label: I18n.t('pageflow_scrolled.editor.section_item.insert_section_below')
+ }, {
+ selected: () =>
+ this.model.chapter.insertSection({after: this.model})
+ }));
+
+ this.appendSubview(new DropDownButtonView({
+ items: dropDownMenuItems,
+ alignMenu: 'right',
+ ellipsisIcon: true,
+ openOnClick: true
+ }), {to: this.ui.dropDownButton});
},
updateActive() {
@@ -73,3 +108,13 @@ export const SectionItemView = Marionette.ItemView.extend({
return active;
}
});
+
+const MenuItem = Backbone.Model.extend({
+ initialize: function(attributes, options) {
+ this.options = options;
+ },
+
+ selected: function() {
+ this.options.selected();
+ }
+});
diff --git a/entry_types/scrolled/package/src/editor/views/SectionItemView.module.css b/entry_types/scrolled/package/src/editor/views/SectionItemView.module.css
index 9d64f0c6ae..eccd8e4659 100644
--- a/entry_types/scrolled/package/src/editor/views/SectionItemView.module.css
+++ b/entry_types/scrolled/package/src/editor/views/SectionItemView.module.css
@@ -46,6 +46,31 @@
opacity: 1;
}
+.dropDownButton {
+ position: absolute;
+ top: 4px;
+ right: 0;
+ z-index: 1;
+}
+
+.dropDownButton button {
+ border: 0 !important;
+ color: var(--ui-on-primary-color) !important;
+ box-shadow: none !important;
+ opacity: 0.3;
+ transition: opacity 0.1s ease;
+ transition-delay: 0.2s;
+}
+
+.invert .dropDownButton button {
+ color: var(--ui-primary-color) !important;
+}
+
+.root:hover .dropDownButton button,
+.dropDownButton button:global(.hover) {
+ opacity: 1;
+}
+
.creating .creatingIndicator { display: block; }
.destroying .destroyingIndicator { display: block; }
.failed .failedIndicator { display: block; }
diff --git a/entry_types/scrolled/package/src/frontend/focusOutline.module.css b/entry_types/scrolled/package/src/frontend/focusOutline.module.css
index 424f533c11..7eb40c119b 100644
--- a/entry_types/scrolled/package/src/frontend/focusOutline.module.css
+++ b/entry_types/scrolled/package/src/frontend/focusOutline.module.css
@@ -1,6 +1,6 @@
-a:focus,
-button:focus,
-[tabindex]:focus {
+body > :global(#root) a:focus,
+body > :global(#root) button:focus,
+body > :global(#root) [tabindex]:focus {
outline: 3px solid #518ad2;
}
diff --git a/entry_types/scrolled/spec/controllers/pageflow_scrolled/editor/sections_controller_spec.rb b/entry_types/scrolled/spec/controllers/pageflow_scrolled/editor/sections_controller_spec.rb
index da1a3d817f..a8711e58ba 100644
--- a/entry_types/scrolled/spec/controllers/pageflow_scrolled/editor/sections_controller_spec.rb
+++ b/entry_types/scrolled/spec/controllers/pageflow_scrolled/editor/sections_controller_spec.rb
@@ -71,6 +71,124 @@ module PageflowScrolled
}, format: 'json')
expect(json_response(path: [:permaId])).to be_present
end
+
+ it 'renders initial content element' do
+ entry = create(:entry, type_name: 'scrolled')
+ chapter = create(:scrolled_chapter, revision: entry.draft)
+
+ authorize_for_editor_controller(entry)
+ post(:create,
+ params: {
+ entry_type: 'scrolled',
+ entry_id: entry,
+ chapter_id: chapter,
+ section: attributes_for(:section)
+ }, format: 'json')
+
+ expect(response.body)
+ .to include_json(contentElements: [
+ {typeName: 'textBlock'}
+ ])
+ end
+ end
+
+ describe '#duplicate' do
+ it 'requires authentication' do
+ entry = create(:entry, type_name: 'scrolled')
+ section = create(:section, revision: entry.draft)
+
+ post(:duplicate,
+ params: {
+ entry_type: 'scrolled',
+ entry_id: entry,
+ id: section.id
+ }, format: 'json')
+
+ expect(response.status).to eq(401)
+ end
+
+ it 'succeeds for authorized user' do
+ entry = create(:entry, type_name: 'scrolled')
+ section = create(:section, revision: entry.draft)
+
+ authorize_for_editor_controller(entry)
+ post(:duplicate,
+ params: {
+ entry_type: 'scrolled',
+ entry_id: entry,
+ id: section.id
+ }, format: 'json')
+
+ expect(response.status).to eq(201)
+ end
+
+ it 'adds duplicate of section in same chapter' do
+ entry = create(:entry, type_name: 'scrolled')
+ section = create(:section, revision: entry.draft, configuration: {'transition' => 'fade'})
+
+ authorize_for_editor_controller(entry)
+ post(:duplicate,
+ params: {
+ entry_type: 'scrolled',
+ entry_id: entry,
+ id: section.id
+ }, format: 'json')
+
+ expect(section.chapter.sections.map(&:configuration))
+ .to eq([{'transition' => 'fade'}, {'transition' => 'fade'}])
+ end
+
+ it 'assigns new perma id' do
+ entry = create(:entry, type_name: 'scrolled')
+ section = create(:section, revision: entry.draft)
+
+ authorize_for_editor_controller(entry)
+ post(:duplicate,
+ params: {
+ entry_type: 'scrolled',
+ entry_id: entry,
+ id: section.id
+ }, format: 'json')
+
+ expect(section.chapter.sections.map(&:perma_id).uniq.size).to eq(2)
+ end
+
+ it 'renders attributes as camel case' do
+ entry = create(:entry, type_name: 'scrolled')
+ section = create(:section, revision: entry.draft, configuration: {'transition' => 'fade'})
+
+ authorize_for_editor_controller(entry)
+ post(:duplicate,
+ params: {
+ entry_type: 'scrolled',
+ entry_id: entry,
+ id: section.id
+ }, format: 'json')
+
+ expect(response.body).to include_json(id: (be > 0),
+ permaId: (be > 0))
+ end
+
+ it 'renders attributes of duplicated content elements' do
+ entry = create(:entry, type_name: 'scrolled')
+ section = create(:section, revision: entry.draft, configuration: {'transition' => 'fade'})
+ create(:content_element, section: section, type_name: 'inlineImage')
+ create(:content_element, section: section, type_name: 'textBlock')
+
+ authorize_for_editor_controller(entry)
+ post(:duplicate,
+ params: {
+ entry_type: 'scrolled',
+ entry_id: entry,
+ id: section.id
+ }, format: 'json')
+
+ expect(response.body)
+ .to include_json(contentElements: [
+ {typeName: 'inlineImage'},
+ {typeName: 'textBlock'}
+ ])
+ end
end
describe '#update' do
diff --git a/entry_types/scrolled/spec/models/pageflow_scrolled/chapter_spec.rb b/entry_types/scrolled/spec/models/pageflow_scrolled/chapter_spec.rb
index 78f6dde710..86a1d25a62 100644
--- a/entry_types/scrolled/spec/models/pageflow_scrolled/chapter_spec.rb
+++ b/entry_types/scrolled/spec/models/pageflow_scrolled/chapter_spec.rb
@@ -27,5 +27,54 @@ module PageflowScrolled
expect(result).to eq([chapter1, chapter2])
end
end
+
+ describe '.create_section' do
+ it 'inserts sections and updates positions' do
+ revision = create(:revision)
+ chapter = create(:scrolled_chapter, revision: revision)
+ section1 = create(:section, chapter: chapter, position: 0)
+ section2 = create(:section, chapter: chapter, position: 1)
+
+ new_section = chapter.create_section(position: 1,
+ configuration: {transition: 'fade'})
+
+ expect(chapter.sections).to eq([section1, new_section, section2])
+ expect(chapter.sections.map(&:position)).to eq([0, 1, 2])
+ end
+
+ it 'creates initial text block' do
+ revision = create(:revision)
+ chapter = create(:scrolled_chapter, revision: revision)
+
+ section = chapter.create_section
+
+ expect(section.content_elements.map(&:type_name)).to eq(['textBlock'])
+ end
+ end
+
+ describe '.duplicate_section' do
+ it 'creates section and content elements' do
+ revision = create(:revision)
+ chapter = create(:scrolled_chapter, revision: revision)
+ section = create(:section, chapter: chapter, position: 0)
+ create(:content_element, section: section, type_name: 'textBlock')
+
+ new_section = chapter.duplicate_section(section)
+
+ expect(chapter.sections).to eq([section, new_section])
+ expect(new_section.content_elements.map(&:type_name)).to eq(['textBlock'])
+ expect(chapter.sections.map(&:position)).to eq([0, 1])
+ end
+
+ it 'returns section with shifted position' do
+ revision = create(:revision)
+ chapter = create(:scrolled_chapter, revision: revision)
+ section = create(:section, chapter: chapter, position: 0)
+
+ new_section = chapter.duplicate_section(section)
+
+ expect(new_section.position).to eq(1)
+ end
+ end
end
end
diff --git a/lib/pageflow/nested_revision_component.rb b/lib/pageflow/nested_revision_component.rb
index 17e9f9da0a..6cf02b3c30 100644
--- a/lib/pageflow/nested_revision_component.rb
+++ b/lib/pageflow/nested_revision_component.rb
@@ -14,10 +14,10 @@ module Container
end
# @api private
- def copy_nested_revision_component_to(record)
+ def copy_nested_revision_component_to(record, reset_perma_ids: false)
nested_revision_component_collection_names.each do |collection_name|
send(collection_name).each do |nested|
- nested.copy_to(record.send(collection_name))
+ nested.copy_to(record.send(collection_name), reset_perma_ids: reset_perma_ids)
end
end
end
@@ -38,12 +38,30 @@ def nested_revision_components(*collection_names)
extend ActiveSupport::Concern
include Container
+ def duplicate
+ copy_with(reset_perma_ids: true) do |record|
+ yield record if block_given?
+ record.save!
+ end
+ end
+
# @api private
- def copy_to(collection)
+ def copy_to(collection, reset_perma_ids: false)
+ copy_with(reset_perma_ids: reset_perma_ids) do |record|
+ collection << record
+ end
+ end
+
+ private
+
+ def copy_with(reset_perma_ids:)
record = dup
- collection << record
+ record.perma_id = nil if reset_perma_ids && record.respond_to?(:perma_id=)
+
+ yield record
+ copy_nested_revision_component_to(record, reset_perma_ids: reset_perma_ids)
- copy_nested_revision_component_to(record)
+ record
end
end
end
diff --git a/package/src/editor/views/DropDownButtonView.js b/package/src/editor/views/DropDownButtonView.js
index f2c32511ab..6ff4afb6b0 100644
--- a/package/src/editor/views/DropDownButtonView.js
+++ b/package/src/editor/views/DropDownButtonView.js
@@ -75,6 +75,7 @@ export const DropDownButtonView = Marionette.ItemView.extend({
this.ui.button.toggleClass('has_icon_and_text', !!this.options.label);
this.ui.button.toggleClass('has_icon_only', !this.options.label);
+ this.ui.button.toggleClass('ellipsis_icon', !!this.options.ellipsisIcon);
this.ui.button.text(this.options.label);
this.ui.button.addClass(this.options.buttonClassName);
diff --git a/package/src/editor/views/inputs/FileInputView.js b/package/src/editor/views/inputs/FileInputView.js
index dc0466c59c..6efb0f46d5 100644
--- a/package/src/editor/views/inputs/FileInputView.js
+++ b/package/src/editor/views/inputs/FileInputView.js
@@ -95,6 +95,7 @@ export const FileInputView = Marionette.ItemView.extend({
if (dropDownMenuItems.length) {
this.appendSubview(new DropDownButtonView({
items: dropDownMenuItems,
+ ellipsisIcon: true,
openOnClick: true
}));
}
diff --git a/package/src/ui/views/mixins/subviewContainer.js b/package/src/ui/views/mixins/subviewContainer.js
index 2b3546fd03..ec392268f1 100644
--- a/package/src/ui/views/mixins/subviewContainer.js
+++ b/package/src/ui/views/mixins/subviewContainer.js
@@ -9,8 +9,8 @@ export const subviewContainer = {
return view;
},
- appendSubview: function(view) {
- return this.$el.append(this.subview(view).el);
+ appendSubview: function(view, {to} = {}) {
+ return (to || this.$el).append(this.subview(view).el);
},
onClose: function() {
diff --git a/spec/pageflow/nested_revision_component_spec.rb b/spec/pageflow/nested_revision_component_spec.rb
new file mode 100644
index 0000000000..86238d1fda
--- /dev/null
+++ b/spec/pageflow/nested_revision_component_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+module Pageflow
+ describe NestedRevisionComponent do
+ describe '#duplicate ' do
+ it 'returns duplcated record' do
+ revision = create(:revision)
+ revision_component = TestCompositeRevisionComponent.create!(revision: revision)
+ nested_revision_component = revision_component.items.create!(text: 'nested')
+
+ result = nested_revision_component.duplicate
+
+ expect(result).to be_persisted
+ expect(result.parent).to eq(nested_revision_component.parent)
+ expect(result.text).to eq(nested_revision_component.text)
+ end
+
+ it 'assigns new perma id' do
+ revision = create(:revision)
+ revision_component = TestCompositeRevisionComponent.create!(revision: revision)
+ nested_revision_component = revision_component.items.create!(text: 'nested')
+
+ result = nested_revision_component.duplicate
+
+ expect(result.perma_id).not_to eq(nested_revision_component.perma_id)
+ end
+
+ it 'duplcates deeply nested revision components' do
+ revision = create(:revision)
+ revision_component = TestCompositeRevisionComponent.create!(revision: revision)
+ nested_revision_component = revision_component.items.create!(text: 'nested')
+ nested_revision_component.items.create!(text: 'deep')
+
+ result = nested_revision_component.duplicate
+
+ expect(result.items.first.text).to eq('deep')
+ end
+
+ it 'assigns new perma ids for deeply nesed revision component' do
+ revision = create(:revision)
+ revision_component = TestCompositeRevisionComponent.create!(revision: revision)
+ nested_revision_component = revision_component.items.create!(text: 'nested')
+ deeply_nested_component = nested_revision_component.items.create!(text: 'deep')
+
+ result = nested_revision_component.duplicate
+
+ expect(result.items.first.perma_id).not_to eq(deeply_nested_component.perma_id)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/test_deeply_nested_revision_component.rb b/spec/support/helpers/test_deeply_nested_revision_component.rb
index 09e5382281..f7b0699b9e 100644
--- a/spec/support/helpers/test_deeply_nested_revision_component.rb
+++ b/spec/support/helpers/test_deeply_nested_revision_component.rb
@@ -3,6 +3,7 @@ class TestDeeplyNestedRevisionComponent < ActiveRecord::Base
self.table_name = :test_deeply_nested_revision_components
include NestedRevisionComponent
+ include AutoGeneratedPermaId
belongs_to :parent, class_name: 'TestNestedRevisionComponent'
end
diff --git a/spec/support/helpers/test_nested_revision_component.rb b/spec/support/helpers/test_nested_revision_component.rb
index 04e8eb5cad..27ee4b5886 100644
--- a/spec/support/helpers/test_nested_revision_component.rb
+++ b/spec/support/helpers/test_nested_revision_component.rb
@@ -3,6 +3,7 @@ class TestNestedRevisionComponent < ActiveRecord::Base
self.table_name = :test_nested_revision_components
include NestedRevisionComponent
+ include AutoGeneratedPermaId
belongs_to :parent, class_name: 'TestCompositeRevisionComponent'
has_many :items, class_name: 'TestDeeplyNestedRevisionComponent', foreign_key: :parent_id