From 05ed5f423b9711f485495567748edf31e4ae895a Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Fri, 22 Sep 2023 15:23:31 +0200 Subject: [PATCH 1/9] Add option to render DropDownButton with ellipsis icon Easier than exposing icon font based icon to scrolled. REDMINE-20217 --- app/assets/stylesheets/pageflow/editor/drop_down_button.scss | 5 +++++ .../stylesheets/pageflow/editor/inputs/file_input.scss | 5 ----- package/src/editor/views/DropDownButtonView.js | 1 + package/src/editor/views/inputs/FileInputView.js | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) 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/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 })); } From 1df3cf5cc1bbdc665f196009eb80c3e929c86aef Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Fri, 22 Sep 2023 15:33:15 +0200 Subject: [PATCH 2/9] Allow duplicating nested revision components Create deep copy and deeply assign new perma ids. REDMINE-20217 --- lib/pageflow/nested_revision_component.rb | 25 +++++++-- .../nested_revision_component_spec.rb | 51 +++++++++++++++++++ .../test_deeply_nested_revision_component.rb | 1 + .../helpers/test_nested_revision_component.rb | 1 + 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 spec/pageflow/nested_revision_component_spec.rb diff --git a/lib/pageflow/nested_revision_component.rb b/lib/pageflow/nested_revision_component.rb index 17e9f9da0a..21ed2ccef4 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,27 @@ def nested_revision_components(*collection_names) extend ActiveSupport::Concern include Container + def duplicate + copy_with(reset_perma_ids: true, &:save!) + 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/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 From 8278e58d08eb82fdb38a2409ce6c830c75c23bf1 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Tue, 26 Sep 2023 12:57:31 +0200 Subject: [PATCH 3/9] Prevent scrolled focus rect from leaking into editor UI Scrolled frontend code is active once section thumbnail has been rendered. Editor UI already implements a more decent focus style. --- .../scrolled/package/src/frontend/focusOutline.module.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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; } From 6e12652a65fd9435f407225028c8d5e403b708a4 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Tue, 26 Sep 2023 14:58:10 +0200 Subject: [PATCH 4/9] Allow inserting and duplicating sections Re-index section positions. REDMINE-20217 --- .../app/models/pageflow_scrolled/chapter.rb | 23 +++++++++ .../models/pageflow_scrolled/chapter_spec.rb | 49 +++++++++++++++++++ lib/pageflow/nested_revision_component.rb | 5 +- 3 files changed, 76 insertions(+), 1 deletion(-) 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/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 21ed2ccef4..6cf02b3c30 100644 --- a/lib/pageflow/nested_revision_component.rb +++ b/lib/pageflow/nested_revision_component.rb @@ -39,7 +39,10 @@ def nested_revision_components(*collection_names) include Container def duplicate - copy_with(reset_perma_ids: true, &:save!) + copy_with(reset_perma_ids: true) do |record| + yield record if block_given? + record.save! + end end # @api private From 76f6c2aa1f398028925d1a4a387258668c80cc3f Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Tue, 26 Sep 2023 14:59:48 +0200 Subject: [PATCH 5/9] Add controller actions to insert and duplicate sections Re-index positions. Render content elements in response to seed newly created section in client without separate request. REDMINE-20217 --- .../editor/sections_controller.rb | 12 +- ...ection_with_content_elements.json.jbuilder | 10 ++ entry_types/scrolled/config/routes.rb | 4 + .../editor/sections_controller_spec.rb | 118 ++++++++++++++++++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 entry_types/scrolled/app/views/pageflow_scrolled/editor/sections/_section_with_content_elements.json.jbuilder 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/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/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/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 From 5f33eb6ac3315e6d9be5c3a34211f791a9b157e8 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Tue, 26 Sep 2023 15:01:21 +0200 Subject: [PATCH 6/9] Allow duplicating and inserting sections via Backbone chapter Use new controller end-points. Optimistically set positions. REDMINE-20217 --- .../spec/editor/models/Chapter-spec.js | 184 +++++++++++++++++- .../package/src/editor/models/Chapter.js | 62 ++++-- 2 files changed, 228 insertions(+), 18 deletions(-) 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..d6a3d785ce 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,55 @@ 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, 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', () => { + section.once('sync', (model, response) => { 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; } }); From ea03d1caa4dbece0d0197aa2d3e300ce0e091f5a Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Tue, 26 Sep 2023 16:58:30 +0200 Subject: [PATCH 7/9] Allow inserting and duplicating sections via SectionItemView REDMINE-20217 --- .../locales/new/duplicate_section.de.yml | 7 +++ .../locales/new/duplicate_section.en.yml | 7 +++ .../src/editor/views/SectionItemView.js | 46 ++++++++++++++++++- .../editor/views/SectionItemView.module.css | 25 ++++++++++ .../src/ui/views/mixins/subviewContainer.js | 4 +- 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 entry_types/scrolled/config/locales/new/duplicate_section.de.yml create mode 100644 entry_types/scrolled/config/locales/new/duplicate_section.en.yml 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/package/src/editor/views/SectionItemView.js b/entry_types/scrolled/package/src/editor/views/SectionItemView.js index 1832d2cd63..ca63f74f70 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() { @@ -63,6 +65,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 +105,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/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() { From a90d35972e5c987bf7c23ce2d8ed0510732200f9 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Tue, 26 Sep 2023 16:59:15 +0200 Subject: [PATCH 8/9] Only select new section when created via "Add section button" Do not leave outline when inserting or duplicating sections. REDMINE-20217 --- entry_types/scrolled/package/src/editor/models/Chapter.js | 6 ++++-- .../scrolled/package/src/editor/views/ChapterItemView.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/entry_types/scrolled/package/src/editor/models/Chapter.js b/entry_types/scrolled/package/src/editor/models/Chapter.js index d6a3d785ce..677890adce 100644 --- a/entry_types/scrolled/package/src/editor/models/Chapter.js +++ b/entry_types/scrolled/package/src/editor/models/Chapter.js @@ -32,7 +32,7 @@ export const Chapter = Backbone.Model.extend({ this.entry = options.entry; }, - addSection(attributes, options) { + addSection(attributes, {select, ...options} = {}) { const section = this.sections.create( new Section( { @@ -51,7 +51,9 @@ export const Chapter = Backbone.Model.extend({ ); section.once('sync', (model, response) => { - this.entry.trigger('selectSectionSettings', section); + if (select) { + this.entry.trigger('selectSectionSettings', section); + } this.entry.trigger('scrollToSection', section); section.configuration.set(response.configuration, {autoSave: false}); 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() { From 67e5ec607685d831f0c6217b0a25b81d8d89b07f Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Tue, 26 Sep 2023 17:12:34 +0200 Subject: [PATCH 9/9] Scroll to active section rendering outline REDMINE-20217 --- .../scrolled/package/src/editor/views/SectionItemView.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/entry_types/scrolled/package/src/editor/views/SectionItemView.js b/entry_types/scrolled/package/src/editor/views/SectionItemView.js index ca63f74f70..aaff20d1a2 100644 --- a/entry_types/scrolled/package/src/editor/views/SectionItemView.js +++ b/entry_types/scrolled/package/src/editor/views/SectionItemView.js @@ -57,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({