diff --git a/.gitignore b/.gitignore
index 9dc2298a..cd03318c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ coverage/
podman.sh
template.dev.rb
tailwind-maglev-pro.config.js
+plugins
# Vite Ruby
/public/vite*
@@ -38,4 +39,4 @@ node_modules
bin/test
spec/dummy/db/*.sqlite3
-spec/legacy_dummy/db/*.sqlite3
\ No newline at end of file
+spec/legacy_dummy/db/*.sqlite3
diff --git a/.ruby-version b/.ruby-version
index 0163af7e..8cf6caf5 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.3.5
\ No newline at end of file
+3.4.1
\ No newline at end of file
diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz
index fc7cfe94..01b6f805 100644
Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ
diff --git a/Gemfile.lock b/Gemfile.lock
index ce1817f8..dd6dea2f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -106,16 +106,16 @@ GEM
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
- ffi (1.17.0-aarch64-linux-gnu)
- ffi (1.17.0-aarch64-linux-musl)
- ffi (1.17.0-arm-linux-gnu)
- ffi (1.17.0-arm-linux-musl)
- ffi (1.17.0-arm64-darwin)
- ffi (1.17.0-x86-linux-gnu)
- ffi (1.17.0-x86-linux-musl)
- ffi (1.17.0-x86_64-darwin)
- ffi (1.17.0-x86_64-linux-gnu)
- ffi (1.17.0-x86_64-linux-musl)
+ ffi (1.17.1-aarch64-linux-gnu)
+ ffi (1.17.1-aarch64-linux-musl)
+ ffi (1.17.1-arm-linux-gnu)
+ ffi (1.17.1-arm-linux-musl)
+ ffi (1.17.1-arm64-darwin)
+ ffi (1.17.1-x86-linux-gnu)
+ ffi (1.17.1-x86-linux-musl)
+ ffi (1.17.1-x86_64-darwin)
+ ffi (1.17.1-x86_64-linux-gnu)
+ ffi (1.17.1-x86_64-linux-musl)
generator_spec (0.10.0)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
@@ -160,6 +160,7 @@ GEM
marcel (1.0.4)
mini_magick (4.13.2)
mini_mime (1.1.5)
+ mini_portile2 (2.8.8)
minitest (5.25.2)
mutex_m (0.3.0)
net-imap (0.5.1)
@@ -172,17 +173,24 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
- nokogiri (1.16.7-aarch64-linux)
+ nokogiri (1.18.6)
+ mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- nokogiri (1.16.7-arm-linux)
+ nokogiri (1.18.6-aarch64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.16.7-arm64-darwin)
+ nokogiri (1.18.6-aarch64-linux-musl)
racc (~> 1.4)
- nokogiri (1.16.7-x86-linux)
+ nokogiri (1.18.6-arm-linux-gnu)
racc (~> 1.4)
- nokogiri (1.16.7-x86_64-darwin)
+ nokogiri (1.18.6-arm-linux-musl)
racc (~> 1.4)
- nokogiri (1.16.7-x86_64-linux)
+ nokogiri (1.18.6-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.6-x86_64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.6-x86_64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.6-x86_64-linux-musl)
racc (~> 1.4)
observer (0.1.2)
ostruct (0.6.1)
diff --git a/Gemfile.rails_7_0 b/Gemfile.rails_7_0
index dbb5d106..9a7352ed 100644
--- a/Gemfile.rails_7_0
+++ b/Gemfile.rails_7_0
@@ -8,7 +8,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# development dependencies will be added by default to the :development group.
gemspec
-gem 'rails', '7.0.8'
+gem 'rails', '~> 7.0', '< 7.1'
# Active Storage analyser
gem 'mini_magick', '~> 4.11'
@@ -45,6 +45,8 @@ gem 'bigdecimal'
gem 'mutex_m'
gem 'drb'
gem 'fiddle'
+gem 'benchmark'
+gem 'concurrent-ruby', '1.3.4'
group :development, :test do
# Use SCSS for stylesheets
@@ -59,6 +61,8 @@ group :development, :test do
gem 'generator_spec'
+ gem 'annotate'
+
gem 'nokogiri', '>= 1.13.10'
end
diff --git a/Gemfile.rails_7_0.lock b/Gemfile.rails_7_0.lock
index 9f2b1dca..66e0f197 100644
--- a/Gemfile.rails_7_0.lock
+++ b/Gemfile.rails_7_0.lock
@@ -12,110 +12,110 @@ PATH
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.0.8)
- actionpack (= 7.0.8)
- activesupport (= 7.0.8)
+ actioncable (7.0.8.7)
+ actionpack (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (7.0.8)
- actionpack (= 7.0.8)
- activejob (= 7.0.8)
- activerecord (= 7.0.8)
- activestorage (= 7.0.8)
- activesupport (= 7.0.8)
+ actionmailbox (7.0.8.7)
+ actionpack (= 7.0.8.7)
+ activejob (= 7.0.8.7)
+ activerecord (= 7.0.8.7)
+ activestorage (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
- actionmailer (7.0.8)
- actionpack (= 7.0.8)
- actionview (= 7.0.8)
- activejob (= 7.0.8)
- activesupport (= 7.0.8)
+ actionmailer (7.0.8.7)
+ actionpack (= 7.0.8.7)
+ actionview (= 7.0.8.7)
+ activejob (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
- actionpack (7.0.8)
- actionview (= 7.0.8)
- activesupport (= 7.0.8)
+ actionpack (7.0.8.7)
+ actionview (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (7.0.8)
- actionpack (= 7.0.8)
- activerecord (= 7.0.8)
- activestorage (= 7.0.8)
- activesupport (= 7.0.8)
+ actiontext (7.0.8.7)
+ actionpack (= 7.0.8.7)
+ activerecord (= 7.0.8.7)
+ activestorage (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.0.8)
- activesupport (= 7.0.8)
+ actionview (7.0.8.7)
+ activesupport (= 7.0.8.7)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (7.0.8)
- activesupport (= 7.0.8)
+ activejob (7.0.8.7)
+ activesupport (= 7.0.8.7)
globalid (>= 0.3.6)
- activemodel (7.0.8)
- activesupport (= 7.0.8)
- activerecord (7.0.8)
- activemodel (= 7.0.8)
- activesupport (= 7.0.8)
- activestorage (7.0.8)
- actionpack (= 7.0.8)
- activejob (= 7.0.8)
- activerecord (= 7.0.8)
- activesupport (= 7.0.8)
+ activemodel (7.0.8.7)
+ activesupport (= 7.0.8.7)
+ activerecord (7.0.8.7)
+ activemodel (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
+ activestorage (7.0.8.7)
+ actionpack (= 7.0.8.7)
+ activejob (= 7.0.8.7)
+ activerecord (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (7.0.8)
+ activesupport (7.0.8.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
- ast (2.4.2)
+ annotate (3.2.0)
+ activerecord (>= 3.2, < 8.0)
+ rake (>= 10.4, < 14.0)
+ ast (2.4.3)
base64 (0.2.0)
bcrypt (3.1.20)
+ benchmark (0.4.0)
bigdecimal (3.1.9)
builder (3.3.0)
concurrent-ruby (1.3.4)
crass (1.0.6)
- date (3.4.0)
- diff-lcs (1.5.1)
+ date (3.4.1)
+ diff-lcs (1.6.1)
docile (1.4.1)
drb (2.2.1)
dry-cli (1.2.0)
- erubi (1.13.0)
+ erubi (1.13.1)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
- ffi (1.17.0-aarch64-linux-gnu)
- ffi (1.17.0-arm-linux-gnu)
- ffi (1.17.0-arm64-darwin)
- ffi (1.17.0-x86-linux-gnu)
- ffi (1.17.0-x86_64-darwin)
- ffi (1.17.0-x86_64-linux-gnu)
- fiddle (1.1.6)
+ ffi (1.17.2)
+ ffi (1.17.2-arm64-darwin)
+ fiddle (1.1.7)
generator_spec (0.10.0)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
globalid (1.2.1)
activesupport (>= 6.1)
- i18n (1.14.6)
+ i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
- jbuilder (2.12.0)
+ jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
- json (2.8.1)
+ json (2.11.2)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -128,9 +128,10 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
- language_server-protocol (3.17.0.3)
- logger (1.6.1)
- loofah (2.23.1)
+ language_server-protocol (3.17.0.4)
+ lint_roller (1.1.0)
+ logger (1.7.0)
+ loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
maglev-injectable (2.1.1)
@@ -143,77 +144,72 @@ GEM
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
- minitest (5.25.1)
+ mini_portile2 (2.8.8)
+ minitest (5.25.5)
mutex_m (0.3.0)
- net-imap (0.5.1)
+ net-imap (0.5.7)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
- net-smtp (0.5.0)
+ net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
- nokogiri (1.16.7-aarch64-linux)
+ nokogiri (1.18.8)
+ mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- nokogiri (1.16.7-arm-linux)
- racc (~> 1.4)
- nokogiri (1.16.7-arm64-darwin)
- racc (~> 1.4)
- nokogiri (1.16.7-x86-linux)
- racc (~> 1.4)
- nokogiri (1.16.7-x86_64-darwin)
- racc (~> 1.4)
- nokogiri (1.16.7-x86_64-linux)
+ nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4)
observer (0.1.2)
ostruct (0.6.1)
- parallel (1.26.3)
- parser (3.3.6.0)
+ parallel (1.27.0)
+ parser (3.3.8.0)
ast (~> 2.4.1)
racc
pg (1.5.9)
- puma (6.5.0)
+ prism (1.4.0)
+ puma (6.6.0)
nio4r (~> 2.0)
racc (1.8.1)
- rack (2.2.10)
+ rack (2.2.13)
rack-proxy (0.7.7)
rack
- rack-test (2.1.0)
+ rack-test (2.2.0)
rack (>= 1.3)
- rails (7.0.8)
- actioncable (= 7.0.8)
- actionmailbox (= 7.0.8)
- actionmailer (= 7.0.8)
- actionpack (= 7.0.8)
- actiontext (= 7.0.8)
- actionview (= 7.0.8)
- activejob (= 7.0.8)
- activemodel (= 7.0.8)
- activerecord (= 7.0.8)
- activestorage (= 7.0.8)
- activesupport (= 7.0.8)
+ rails (7.0.8.7)
+ actioncable (= 7.0.8.7)
+ actionmailbox (= 7.0.8.7)
+ actionmailer (= 7.0.8.7)
+ actionpack (= 7.0.8.7)
+ actiontext (= 7.0.8.7)
+ actionview (= 7.0.8.7)
+ activejob (= 7.0.8.7)
+ activemodel (= 7.0.8.7)
+ activerecord (= 7.0.8.7)
+ activestorage (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
bundler (>= 1.15.0)
- railties (= 7.0.8)
+ railties (= 7.0.8.7)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
- rails-html-sanitizer (1.6.0)
+ rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
- nokogiri (~> 1.14)
- railties (7.0.8)
- actionpack (= 7.0.8)
- activesupport (= 7.0.8)
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
+ railties (7.0.8.7)
+ actionpack (= 7.0.8.7)
+ activesupport (= 7.0.8.7)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.2.1)
- regexp_parser (2.9.2)
- rspec-core (3.13.2)
+ regexp_parser (2.10.0)
+ rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
@@ -221,7 +217,7 @@ GEM
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-rails (7.1.0)
+ rspec-rails (7.1.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
@@ -229,46 +225,54 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
- rspec-support (3.13.1)
- rubocop (1.68.0)
+ rspec-support (3.13.2)
+ rubocop (1.75.3)
json (~> 2.3)
- language_server-protocol (>= 3.17.0)
+ language_server-protocol (~> 3.17.0.2)
+ lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
- regexp_parser (>= 2.4, < 3.0)
- rubocop-ast (>= 1.32.2, < 2.0)
+ regexp_parser (>= 2.9.3, < 3.0)
+ rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.35.0)
- parser (>= 3.3.1.0)
- rubocop-md (1.2.4)
- rubocop (>= 1.45)
- rubocop-minitest (0.36.0)
- rubocop (>= 1.61, < 2.0)
- rubocop-ast (>= 1.31.1, < 2.0)
- rubocop-packaging (0.5.2)
- rubocop (>= 1.33, < 2.0)
- rubocop-performance (1.22.1)
- rubocop (>= 1.48.1, < 2.0)
- rubocop-ast (>= 1.31.1, < 2.0)
- rubocop-rails (2.27.0)
+ unicode-display_width (>= 2.4.0, < 4.0)
+ rubocop-ast (1.44.1)
+ parser (>= 3.3.7.2)
+ prism (~> 1.4)
+ rubocop-md (2.0.1)
+ lint_roller (~> 1.1)
+ rubocop (>= 1.72.1)
+ rubocop-minitest (0.38.0)
+ lint_roller (~> 1.1)
+ rubocop (>= 1.75.0, < 2.0)
+ rubocop-ast (>= 1.38.0, < 2.0)
+ rubocop-packaging (0.6.0)
+ lint_roller (~> 1.1.0)
+ rubocop (>= 1.72.1, < 2.0)
+ rubocop-performance (1.25.0)
+ lint_roller (~> 1.1)
+ rubocop (>= 1.75.0, < 2.0)
+ rubocop-ast (>= 1.38.0, < 2.0)
+ rubocop-rails (2.31.0)
activesupport (>= 4.2.0)
+ lint_roller (~> 1.1)
rack (>= 1.1)
- rubocop (>= 1.52.0, < 2.0)
- rubocop-ast (>= 1.31.1, < 2.0)
- rubocop-rails_config (1.16.0)
- rubocop (>= 1.57.0)
- rubocop-ast (>= 1.26.0)
+ rubocop (>= 1.75.0, < 2.0)
+ rubocop-ast (>= 1.38.0, < 2.0)
+ rubocop-rails_config (1.17.1)
+ rubocop (>= 1.72.2)
+ rubocop-ast (>= 1.38.0)
rubocop-md
- rubocop-minitest (~> 0.22)
- rubocop-packaging (~> 0.5)
- rubocop-performance (~> 1.11)
- rubocop-rails (~> 2.0)
- rubocop-rspec (3.2.0)
- rubocop (~> 1.61)
+ rubocop-minitest (~> 0.37)
+ rubocop-packaging (~> 0.6)
+ rubocop-performance (~> 1.24)
+ rubocop-rails (~> 2.30)
+ rubocop-rspec (3.6.0)
+ lint_roller (~> 1.1)
+ rubocop (~> 1.72, >= 1.72.1)
ruby-progressbar (1.13.0)
- ruby-vips (2.2.2)
+ ruby-vips (2.2.3)
ffi (~> 1.12)
logger
simplecov (0.22.0)
@@ -277,49 +281,49 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
- sprockets (4.2.1)
+ sprockets (4.2.2)
concurrent-ruby (~> 1.0)
+ logger
rack (>= 2.2.4, < 4)
sprockets-rails (3.5.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
- sqlite3 (1.7.3-aarch64-linux)
- sqlite3 (1.7.3-arm-linux)
- sqlite3 (1.7.3-arm64-darwin)
- sqlite3 (1.7.3-x86-linux)
- sqlite3 (1.7.3-x86_64-darwin)
- sqlite3 (1.7.3-x86_64-linux)
+ sqlite3 (1.7.3)
+ mini_portile2 (~> 2.8.0)
thor (1.3.2)
- timeout (0.4.2)
+ timeout (0.4.3)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
- unicode-display_width (2.6.0)
+ unicode-display_width (3.1.4)
+ unicode-emoji (~> 4.0, >= 4.0.4)
+ unicode-emoji (4.0.4)
vite_rails (3.0.19)
railties (>= 5.1, < 9)
vite_ruby (~> 3.0, >= 3.2.2)
- vite_ruby (3.9.0)
+ vite_ruby (3.9.2)
dry-cli (>= 0.7, < 2)
logger (~> 1.6)
+ mutex_m
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
- websocket-driver (0.7.6)
+ websocket-driver (0.7.7)
+ base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
- zeitwerk (2.7.1)
+ zeitwerk (2.7.2)
PLATFORMS
- aarch64-linux
- arm-linux
- arm64-darwin
- x86-linux
- x86_64-darwin
- x86_64-linux
+ arm64-darwin-24
+ ruby
DEPENDENCIES
+ annotate
base64
bcrypt
+ benchmark
bigdecimal
+ concurrent-ruby (= 1.3.4)
drb
factory_bot_rails (~> 6.2.0)
fiddle
@@ -333,7 +337,7 @@ DEPENDENCIES
ostruct
pg (~> 1.5.9)
puma
- rails (= 7.0.8)
+ rails (~> 7.0, < 7.1)
rspec-rails
rubocop
rubocop-rails_config
@@ -343,4 +347,4 @@ DEPENDENCIES
sqlite3 (~> 1.4)
BUNDLED WITH
- 2.5.4
+ 2.6.2
diff --git a/Gemfile.rails_7_2 b/Gemfile.rails_7_2
index 6039f4f8..5109cbba 100644
--- a/Gemfile.rails_7_2
+++ b/Gemfile.rails_7_2
@@ -33,6 +33,10 @@ gem 'puma'
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.
+# Gems not part of the standard library anymore
+gem 'observer'
+gem 'ostruct'
+
# To use a debugger
# gem 'byebug', group: [:development, :test]
@@ -40,10 +44,6 @@ gem 'puma'
gem 'pg', '~> 1.5.9'
gem 'sqlite3'
-# Gems no longer be part of the default gems from Ruby 3.5.0
-gem 'observer'
-gem 'ostruct'
-
group :development, :test do
# Use SCSS for stylesheets
gem 'bcrypt'
diff --git a/Gemfile.rails_7_2.lock b/Gemfile.rails_7_2.lock
index c37a1236..1aed1162 100644
--- a/Gemfile.rails_7_2.lock
+++ b/Gemfile.rails_7_2.lock
@@ -106,16 +106,16 @@ GEM
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
- ffi (1.17.0-aarch64-linux-gnu)
- ffi (1.17.0-aarch64-linux-musl)
- ffi (1.17.0-arm-linux-gnu)
- ffi (1.17.0-arm-linux-musl)
- ffi (1.17.0-arm64-darwin)
- ffi (1.17.0-x86-linux-gnu)
- ffi (1.17.0-x86-linux-musl)
- ffi (1.17.0-x86_64-darwin)
- ffi (1.17.0-x86_64-linux-gnu)
- ffi (1.17.0-x86_64-linux-musl)
+ ffi (1.17.1-aarch64-linux-gnu)
+ ffi (1.17.1-aarch64-linux-musl)
+ ffi (1.17.1-arm-linux-gnu)
+ ffi (1.17.1-arm-linux-musl)
+ ffi (1.17.1-arm64-darwin)
+ ffi (1.17.1-x86-linux-gnu)
+ ffi (1.17.1-x86-linux-musl)
+ ffi (1.17.1-x86_64-darwin)
+ ffi (1.17.1-x86_64-linux-gnu)
+ ffi (1.17.1-x86_64-linux-musl)
generator_spec (0.10.0)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
@@ -160,6 +160,7 @@ GEM
marcel (1.0.4)
mini_magick (4.13.2)
mini_mime (1.1.5)
+ mini_portile2 (2.8.8)
minitest (5.25.2)
mutex_m (0.3.0)
net-imap (0.5.1)
@@ -172,17 +173,24 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
- nokogiri (1.16.7-aarch64-linux)
+ nokogiri (1.18.7)
+ mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- nokogiri (1.16.7-arm-linux)
+ nokogiri (1.18.7-aarch64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.16.7-arm64-darwin)
+ nokogiri (1.18.7-aarch64-linux-musl)
racc (~> 1.4)
- nokogiri (1.16.7-x86-linux)
+ nokogiri (1.18.7-arm-linux-gnu)
racc (~> 1.4)
- nokogiri (1.16.7-x86_64-darwin)
+ nokogiri (1.18.7-arm-linux-musl)
racc (~> 1.4)
- nokogiri (1.16.7-x86_64-linux)
+ nokogiri (1.18.7-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.7-x86_64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.18.7-x86_64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.18.7-x86_64-linux-musl)
racc (~> 1.4)
observer (0.1.2)
ostruct (0.6.1)
diff --git a/app/controllers/concerns/maglev/fetchers_concern.rb b/app/controllers/concerns/maglev/fetchers_concern.rb
index 5a866be5..efb21518 100644
--- a/app/controllers/concerns/maglev/fetchers_concern.rb
+++ b/app/controllers/concerns/maglev/fetchers_concern.rb
@@ -35,11 +35,12 @@ def fetch_maglev_page
)
end
- def fetch_maglev_page_sections(page_sections = nil)
+ def fetch_maglev_page_sections(sections_content = nil)
@fetch_maglev_page_sections ||= maglev_services.get_page_sections.call(
page: fetch_maglev_page,
- page_sections: page_sections,
- locale: content_locale
+ sections_content: sections_content,
+ locale: content_locale,
+ include_deleted: maglev_include_deleted_sections?
)
end
@@ -92,6 +93,10 @@ def maglev_page_sections
fetch_maglev_page_sections
end
+ def maglev_site_scoped_sections
+ maglev_services.get_site_scoped_sections.call
+ end
+
def maglev_sections_path
fetch_maglev_sections_path
end
@@ -120,5 +125,9 @@ def maglev_style
theme: maglev_theme
)
end
+
+ def maglev_include_deleted_sections?
+ false
+ end
end
end
diff --git a/app/controllers/concerns/maglev/standalone_sections_concern.rb b/app/controllers/concerns/maglev/in_app_rendering_concern.rb
similarity index 63%
rename from app/controllers/concerns/maglev/standalone_sections_concern.rb
rename to app/controllers/concerns/maglev/in_app_rendering_concern.rb
index d38a0cc3..786e19ea 100644
--- a/app/controllers/concerns/maglev/standalone_sections_concern.rb
+++ b/app/controllers/concerns/maglev/in_app_rendering_concern.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Maglev
- module StandaloneSectionsConcern
+ module InAppRenderingConcern
extend ActiveSupport::Concern
included do
@@ -20,21 +20,28 @@ def maglev_rendering_mode
private
- def fetch_maglev_site_scoped_sections
+ def fetch_maglev_sections_content(layout_id: nil)
return if within_maglev_engine?
fetch_maglev_site
fetch_maglev_theme
- fetch_maglev_dummy_page
+ fetch_maglev_dummy_page(layout_id)
fetch_maglev_page_sections
end
- def fetch_maglev_dummy_page
- @fetch_maglev_page = ::Maglev::Page.new(title: 'DummyPage', sections: fetch_maglev_site.sections)
+ def fetch_maglev_dummy_page(layout_id = nil)
+ @fetch_maglev_page = ::Maglev::Page.new(
+ title: 'DummyPage',
+ layout_id: layout_id || maglev_layout_id
+ )
end
def within_maglev_engine?
controller_path.starts_with?('maglev/')
end
+
+ def maglev_layout_id
+ nil
+ end
end
end
diff --git a/app/controllers/maglev/admin/sections/previews_controller.rb b/app/controllers/maglev/admin/sections/previews_controller.rb
index f403c7e7..265337dc 100644
--- a/app/controllers/maglev/admin/sections/previews_controller.rb
+++ b/app/controllers/maglev/admin/sections/previews_controller.rb
@@ -23,10 +23,14 @@ def fetch_maglev_page
Maglev::Page.new(
title: 'Preview section',
path: 'preview',
- sections: [fetch_section!.build_default_content]
+ layout_id: 'preview'
)
end
+ def fetch_maglev_page_sections
+ [fetch_section!.build_default_content.with_indifferent_access]
+ end
+
def fetch_section!
fetch_section || (raise ::Maglev::Errors::UnknownSection, "Unknown section #{params[:id]}")
end
diff --git a/app/controllers/maglev/api/pages_controller.rb b/app/controllers/maglev/api/pages_controller.rb
index fe0b0ac3..691c3c06 100644
--- a/app/controllers/maglev/api/pages_controller.rb
+++ b/app/controllers/maglev/api/pages_controller.rb
@@ -15,46 +15,28 @@ def show
end
def create
- page = persist!(resources.new)
+ page = resources.create!(page_params)
head :created, location: api_page_path(page)
end
- def destroy
- resources.destroy(params[:id])
- head :no_content
- end
-
def update
page = resources.find(params[:id])
- persist!(page)
+ page.update!(page_params)
head :ok, page_lock_version: page.lock_version
end
+ def destroy
+ resources.destroy(params[:id])
+ head :no_content
+ end
+
private
def page_params
- params.require(:page).permit(:title, :path,
+ params.require(:page).permit(:title, :path, :layout_id,
:seo_title, :meta_description,
:og_title, :og_description, :og_image_url,
- :visible, :lock_version).tap do |whitelisted|
- whitelisted[:sections] = params[:page].to_unsafe_hash[:sections] unless params.dig(:page, :sections).nil?
- end
- end
-
- def site_params
- lock_version = params.dig(:site, :lock_version)
- sections = params[:site].to_unsafe_hash[:sections] unless params.dig(:site, :sections).nil?
- style = params.dig(:site, :style)
- (lock_version && sections ? { lock_version: lock_version, sections: sections } : {}).merge(style: style)
- end
-
- def persist!(page)
- services.persist_page.call(
- page: page,
- page_attributes: page_params,
- site: maglev_site,
- site_attributes: site_params
- )
+ :visible, :lock_version)
end
def resources
diff --git a/app/controllers/maglev/api/sections_content_controller.rb b/app/controllers/maglev/api/sections_content_controller.rb
new file mode 100644
index 00000000..60d27ba5
--- /dev/null
+++ b/app/controllers/maglev/api/sections_content_controller.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Maglev
+ module Api
+ class SectionsContentController < ::Maglev::ApiController
+ before_action :set_page
+
+ def show
+ @sections_content = maglev_services.get_page_sections.call(
+ page: @page,
+ locale: content_locale,
+ include_deleted: true
+ )
+ end
+
+ def update
+ @stores = services.persist_sections_content.call(
+ site: maglev_site,
+ page: @page,
+ sections_content: sections_content_params
+ )
+ end
+
+ private
+
+ def set_page
+ @page = resources.find(params[:page_id])
+ end
+
+ def sections_content_params
+ params.to_unsafe_hash[:sections_content]
+ end
+
+ def resources
+ ::Maglev::Page
+ end
+ end
+ end
+end
diff --git a/app/controllers/maglev/api/sites_controller.rb b/app/controllers/maglev/api/sites_controller.rb
index 66086490..879c4b2c 100644
--- a/app/controllers/maglev/api/sites_controller.rb
+++ b/app/controllers/maglev/api/sites_controller.rb
@@ -6,6 +6,7 @@ class SitesController < ::Maglev::ApiController
def show
if (@site = maglev_site).present?
@home_page_id = maglev_page_collection.home.pick(:id)
+ @number_of_pages = maglev_page_collection.count
else
head :not_found
end
diff --git a/app/controllers/maglev/api/styles_controller.rb b/app/controllers/maglev/api/styles_controller.rb
new file mode 100644
index 00000000..01c87ee9
--- /dev/null
+++ b/app/controllers/maglev/api/styles_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Maglev
+ module Api
+ class StylesController < ::Maglev::ApiController
+ def update
+ @site = maglev_site
+ @site.update(style: style_params)
+ head :ok
+ end
+
+ private
+
+ def style_params
+ params.to_unsafe_hash[:style]
+ end
+ end
+ end
+end
diff --git a/app/controllers/maglev/api_controller.rb b/app/controllers/maglev/api_controller.rb
index fa32e137..d1d0ea03 100644
--- a/app/controllers/maglev/api_controller.rb
+++ b/app/controllers/maglev/api_controller.rb
@@ -38,6 +38,10 @@ def maglev_theme
@maglev_theme ||= maglev_services.fetch_theme.call
end
+ def maglev_rendering_mode
+ params[:rendering_mode] || :editor
+ end
+
def record_errors(exception)
render(json: { errors: exception.record.errors }, status: :bad_request)
end
diff --git a/app/controllers/maglev/editor_controller.rb b/app/controllers/maglev/editor_controller.rb
index 7ac37259..0a04116c 100644
--- a/app/controllers/maglev/editor_controller.rb
+++ b/app/controllers/maglev/editor_controller.rb
@@ -12,7 +12,7 @@ class EditorController < ApplicationController
before_action :ensure_path_and_content_locale, only: :show
before_action :set_content_locale, only: :show
- helper_method :maglev_home_page_id
+ helper_method :maglev_home_page_id, :maglev_pages_count, :maglev_site_scoped_sections
def show
fetch_maglev_page_content
@@ -37,6 +37,10 @@ def maglev_home_page_id
maglev_pages_collection.home(default_content_locale).pick(:id)
end
+ def maglev_pages_count
+ @maglev_pages_count ||= maglev_pages_collection.count
+ end
+
def maglev_pages_collection
::Maglev::Page
end
@@ -49,6 +53,10 @@ def maglev_rendering_mode
:editor
end
+ def maglev_include_deleted_sections?
+ true
+ end
+
def default_maglev_editor_path
editor_path(
params[:path] || 'index',
diff --git a/app/controllers/maglev/page_preview_controller.rb b/app/controllers/maglev/page_preview_controller.rb
index 52ed7bec..b53c5a21 100644
--- a/app/controllers/maglev/page_preview_controller.rb
+++ b/app/controllers/maglev/page_preview_controller.rb
@@ -32,7 +32,7 @@ def fetch_maglev_site
def fetch_maglev_page_sections
return super if action_name == 'index'
- super(JSON.parse(params[:page_sections]))
+ super(JSON.parse(params[:sections_content]))
end
def maglev_rendering_mode
diff --git a/app/frontend/editor/assets/remixicons/ri-links-line.svg b/app/frontend/editor/assets/remixicons/ri-links-line.svg
new file mode 100644
index 00000000..b4fd9602
--- /dev/null
+++ b/app/frontend/editor/assets/remixicons/ri-links-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/frontend/editor/components/header-nav/save-button.vue b/app/frontend/editor/components/header-nav/save-button.vue
index 675996ae..3f9e3699 100644
--- a/app/frontend/editor/components/header-nav/save-button.vue
+++ b/app/frontend/editor/components/header-nav/save-button.vue
@@ -26,7 +26,7 @@ export default {
},
methods: {
async save() {
- await this.$store.dispatch('persistPage')
+ await this.$store.dispatch('persistSectionsContent')
},
},
}
diff --git a/app/frontend/editor/components/kit/accordion.vue b/app/frontend/editor/components/kit/accordion.vue
index a842cc71..7fa5bfb0 100644
--- a/app/frontend/editor/components/kit/accordion.vue
+++ b/app/frontend/editor/components/kit/accordion.vue
@@ -8,16 +8,9 @@
{{ $t('mirrorSectionSetup.protectedMessage.message') }}
+{{ $t('sections.listPane.empty.title') }}
+
+
{{ $t('themeSectionList.empty.message') }}
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque odio purus, suscipit nec arcu id, tempor feugiat risus. Maecenas cursus vehicula sagittis. Nulla varius sagittis nunc eget iaculis.
', - }, - { - id: 'image', - value: { - id: 14, - url: 'http://localhost:3000/maglev/assets/14', - width: 516, - height: 320, - filename: 'img-91-3.jpg', - byteSize: 41683, - }, - }, - { id: 'button', value: { url: '#', text: 'Click here' } }, - ], - }, - 'xM6f-kyh': { - id: 'xM6f-kyh', - type: 'list_01', - blocks: ['fNIEuzF0', 'UVGOFAI5', 'K3Xotn7f', 'Pst6WyU0'], - settings: [ - { id: 'title', value: 'Home [EN] v1.2.11' }, - { - id: 'body', - value: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque odio purus, suscipit nec arcu id, tempor feugiat risus. Maecenas cursus vehicula sagittis. Nulla varius sagittis nunc eget iaculis.
', - }, - { id: 'background_color', value: '#BFDBFE' }, - ], - }, - }, - page: { - 1: { - sections: ['GrYZW-VP', '8hKSujtd', 'xM6f-kyh'], - id: 1, - title: 'Home page', - path: 'index', - pathHash: { en: 'index', fr: 'index' }, - visible: true, - seoTitle: 'My awesome home page', - metaDescription: null, - ogTitle: null, - ogDescription: null, - ogImageUrl: null, - previewUrl: '/maglev/preview', - sectionNames: [ - { id: 'GrYZW-VP', name: 'Navbar 01' }, - { id: '8hKSujtd', name: 'Content #1' }, - { id: 'xM6f-kyh', name: 'List #1' }, - ], - lockVersion: 1, - translated: true, - }, - }, - }, - result: 1, -} +} \ No newline at end of file diff --git a/app/frontend/editor/spec/__mocks__/sections-content.js b/app/frontend/editor/spec/__mocks__/sections-content.js new file mode 100644 index 00000000..9560c195 --- /dev/null +++ b/app/frontend/editor/spec/__mocks__/sections-content.js @@ -0,0 +1,415 @@ +export const headerSections = [ + { + id: 'GrYZW-VP', + type: 'navbar_01', + deleted: false, + foobar: '42', + blocks: [ + { + id: 'RiEo8C3f', + type: 'navbar_item', + settings: [ + { + id: 'link', + value: { href: '#', text: 'Home', linkType: 'url' }, + }, + ], + }, + { + id: 'P1fGieWs', + type: 'navbar_item', + settings: [ + { + id: 'link', + value: { href: '#', text: 'About us', linkType: 'url' }, + }, + ], + }, + { + id: 'sDo-Dg85', + type: 'navbar_item', + settings: [ + { + id: 'link', + value: { + href: '//contact', + text: 'Contact', + email: null, + linkId: 9, + linkType: 'page', + linkLabel: 'Contact us', + sectionId: null, + openNewWindow: false, + }, + }, + ], + }, + { + id: 'K-C_zRcH', + type: 'navbar_item', + settings: [ + { + id: 'link', + value: { + href: '/products', + text: 'Products #1', + email: null, + linkId: 'd870133f9a075477a96a58e7639d40c5', + linkType: 'page', + linkLabel: 'Products', + sectionId: null, + openNewWindow: true, + }, + }, + ], + }, + ], + settings: [ + { + id: 'logo', + value: { + id: 15, + url: 'http://localhost:3000/maglev/assets/15', + width: 572, + height: 290, + filename: 'Screen Shot 2021-06-30 at 3.44.04 PM.png', + byteSize: 35070, + }, + }, + ] + } +] + +export const mainSections = [ + { + id: '8hKSujtd', + type: 'content_01', + blocks: [], + settings: [ + { id: 'pre_title', value: 'preTitle' }, + { id: 'title', value: 'My awesome title!' }, + { + id: 'body', + value: + '\u003cp\u003eLorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque odio purus, suscipit nec arcu id, tempor feugiat risus. Maecenas cursus vehicula sagittis. Nulla varius sagittis nunc eget iaculis.\u003c/p\u003e', + }, + { + id: 'image', + value: { + id: 14, + url: 'http://localhost:3000/maglev/assets/14', + width: 516, + height: 320, + filename: 'img-91-3.jpg', + byteSize: 41683, + }, + }, + { id: 'button', value: { url: '#', text: 'Click here' } }, + ], + }, + { + id: 'xM6f-kyh', + type: 'list_01', + blocks: [ + { + id: 'fNIEuzF0', + type: 'list_item', + settings: [ + { id: 'title', value: 'Item title' }, + { + id: 'image', + value: { + id: 13, + url: 'http://localhost:3000/maglev/assets/13', + width: 1920, + height: 1200, + filename: 'img-97.jpg', + byteSize: 458107, + }, + }, + ], + }, + { + id: 'UVGOFAI5', + type: 'list_item', + settings: [ + { id: 'title', value: 'Item title' }, + { + id: 'image', + value: { + id: 9, + url: 'http://localhost:3000/maglev/assets/9', + width: 516, + height: 400, + filename: 'inner-74-2.jpg', + byteSize: 63881, + }, + }, + ], + }, + { + id: 'K3Xotn7f', + type: 'list_item', + settings: [ + { id: 'title', value: 'Item title' }, + { + id: 'image', + value: { + id: 12, + url: 'http://localhost:3000/maglev/assets/12', + width: 516, + height: 320, + filename: 'img-91-4.jpg', + byteSize: 21808, + }, + }, + ], + }, + { + id: 'Pst6WyU0', + type: 'list_item', + settings: [ + { id: 'title', value: 'Item title' }, + { + id: 'image', + value: { + id: 11, + url: 'http://localhost:3000/maglev/assets/11', + width: 1920, + height: 1200, + filename: 'img-91.jpg', + byteSize: 180178, + }, + }, + ], + }, + ], + settings: [ + { id: 'title', value: 'Home [EN] v1.2.11' }, + { + id: 'body', + value: + '\u003cp\u003eLorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque odio purus, suscipit nec arcu id, tempor feugiat risus. Maecenas cursus vehicula sagittis. Nulla \u003cstrong\u003e\u003cu\u003e\u003ca href="/" rel="noopener noreferrer nofollow" maglev-link-type="page" maglev-link-id="7"\u003evarius\u003c/a\u003e\u003c/u\u003e\u003c/strong\u003e sagittis nunc eget iaculis.\u003c/p\u003e', + }, + { id: 'background_color', value: '#BFDBFE' }, + ], + } +] + +export const sectionsContent = [ + { + id: 'header', + sections: headerSections, + lockVersion: 1, + }, + { + id: 'main', + sections: mainSections, + lockVersion: 1 + }, + { + id: 'footer', + sections: [], + lockVersion: 1 + } +] + +export const normalizedSectionsContent = { + entities: { + blocks: { + RiEo8C3f: { + id: 'RiEo8C3f', + type: 'navbar_item', + settings: [ + { id: 'link', value: { href: '#', text: 'Home', linkType: 'url' } }, + ], + }, + P1fGieWs: { + id: 'P1fGieWs', + type: 'navbar_item', + settings: [ + { + id: 'link', + value: { href: '#', text: 'About us', linkType: 'url' }, + }, + ], + }, + 'sDo-Dg85': { + id: 'sDo-Dg85', + type: 'navbar_item', + settings: [ + { + id: 'link', + value: { + href: '//contact', + text: 'Contact', + email: null, + linkId: 9, + linkType: 'page', + linkLabel: 'Contact us', + sectionId: null, + openNewWindow: false, + }, + }, + ], + }, + 'K-C_zRcH': { + id: 'K-C_zRcH', + type: 'navbar_item', + settings: [ + { + id: 'link', + value: { + href: '/products', + text: 'Products #1', + email: null, + linkId: 'd870133f9a075477a96a58e7639d40c5', + linkType: 'page', + linkLabel: 'Products', + sectionId: null, + openNewWindow: true, + }, + }, + ], + }, + fNIEuzF0: { + id: 'fNIEuzF0', + type: 'list_item', + settings: [ + { id: 'title', value: 'Item title' }, + { + id: 'image', + value: { + id: 13, + url: 'http://localhost:3000/maglev/assets/13', + width: 1920, + height: 1200, + filename: 'img-97.jpg', + byteSize: 458107, + }, + }, + ], + }, + UVGOFAI5: { + id: 'UVGOFAI5', + type: 'list_item', + settings: [ + { id: 'title', value: 'Item title' }, + { + id: 'image', + value: { + id: 9, + url: 'http://localhost:3000/maglev/assets/9', + width: 516, + height: 400, + filename: 'inner-74-2.jpg', + byteSize: 63881, + }, + }, + ], + }, + K3Xotn7f: { + id: 'K3Xotn7f', + type: 'list_item', + settings: [ + { id: 'title', value: 'Item title' }, + { + id: 'image', + value: { + id: 12, + url: 'http://localhost:3000/maglev/assets/12', + width: 516, + height: 320, + filename: 'img-91-4.jpg', + byteSize: 21808, + }, + }, + ], + }, + Pst6WyU0: { + id: 'Pst6WyU0', + type: 'list_item', + settings: [ + { id: 'title', value: 'Item title' }, + { + id: 'image', + value: { + id: 11, + url: 'http://localhost:3000/maglev/assets/11', + width: 1920, + height: 1200, + filename: 'img-91.jpg', + byteSize: 180178, + }, + }, + ], + }, + }, + sections: { + 'GrYZW-VP': { + id: 'GrYZW-VP', + type: 'navbar_01', + blocks: ['RiEo8C3f', 'P1fGieWs', 'sDo-Dg85', 'K-C_zRcH'], + settings: [ + { + id: 'logo', + value: { + id: 15, + url: 'http://localhost:3000/maglev/assets/15', + width: 572, + height: 290, + filename: 'Screen Shot 2021-06-30 at 3.44.04 PM.png', + byteSize: 35070, + }, + }, + ], + }, + '8hKSujtd': { + id: '8hKSujtd', + type: 'content_01', + blocks: [], + settings: [ + { id: 'pre_title', value: 'preTitle' }, + { id: 'title', value: 'My awesome title!' }, + { + id: 'body', + value: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque odio purus, suscipit nec arcu id, tempor feugiat risus. Maecenas cursus vehicula sagittis. Nulla varius sagittis nunc eget iaculis.
', + }, + { + id: 'image', + value: { + id: 14, + url: 'http://localhost:3000/maglev/assets/14', + width: 516, + height: 320, + filename: 'img-91-3.jpg', + byteSize: 41683, + }, + }, + { id: 'button', value: { url: '#', text: 'Click here' } }, + ], + }, + 'xM6f-kyh': { + id: 'xM6f-kyh', + type: 'list_01', + blocks: ['fNIEuzF0', 'UVGOFAI5', 'K3Xotn7f', 'Pst6WyU0'], + settings: [ + { id: 'title', value: 'Home [EN] v1.2.11' }, + { + id: 'body', + value: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque odio purus, suscipit nec arcu id, tempor feugiat risus. Maecenas cursus vehicula sagittis. Nulla varius sagittis nunc eget iaculis.
', + }, + { id: 'background_color', value: '#BFDBFE' }, + ], + }, + }, + layoutGroups: { + header: { id: 'header', sections: [ 'GrYZW-VP' ], lockVersion: 1 }, + main: { id: 'main', sections: [ '8hKSujtd', 'xM6f-kyh' ], lockVersion: 1 }, + footer: { id: 'footer', sections: [], lockVersion: 1 } + }, + }, + result: [ 'header', 'main', 'footer' ] +} diff --git a/app/frontend/editor/spec/__mocks__/services.js b/app/frontend/editor/spec/__mocks__/services.js index ce3ea0eb..2de8b800 100644 --- a/app/frontend/editor/spec/__mocks__/services.js +++ b/app/frontend/editor/spec/__mocks__/services.js @@ -27,14 +27,19 @@ const pageService = { updateSettings: vi.fn(), setVisible: vi.fn(), clone: vi.fn(), - destroy: vi.fn(), + destroy: vi.fn() +} + +const sectionsContentService = { normalize: vi.fn(), denormalize: vi.fn(), + find: vi.fn(), + update: vi.fn(), + findSingleSection: vi.fn() } const sectionService = { calculateMovingIndices: vi.fn(), - canBeAddedToPage: vi.fn(), normalize: vi.fn(), build: vi.fn(), getSettings: vi.fn(), @@ -75,6 +80,7 @@ export default { site: siteService, theme: themeService, page: pageService, + sectionsContent: sectionsContentService, section: sectionService, block: blockService, image: imageService, diff --git a/app/frontend/editor/spec/__mocks__/site.js b/app/frontend/editor/spec/__mocks__/site.js index fef49241..f70b0860 100644 --- a/app/frontend/editor/spec/__mocks__/site.js +++ b/app/frontend/editor/spec/__mocks__/site.js @@ -1,89 +1,4 @@ export const site = { - sections: [ - { - id: 'BLpzw147', - type: 'footer_01', - blocks: [], - settings: [ - { id: 'title', value: 'Title' }, - { id: 'foo', value: 'icon-iconmonstr-target-3-icon' }, - ], - }, - { - id: 'sB8sSLsh', - type: 'navbar_01', - blocks: [ - { - id: 'RiEo8C3f', - type: 'navbar_item', - settings: [ - { id: 'link', value: { href: '#', text: 'Home', linkType: 'url' } }, - ], - }, - { - id: 'P1fGieWs', - type: 'navbar_item', - settings: [ - { - id: 'link', - value: { href: '#', text: 'About us', linkType: 'url' }, - }, - ], - }, - { - id: 'sDo-Dg85', - type: 'navbar_item', - settings: [ - { - id: 'link', - value: { - href: '//contact', - text: 'Contact', - email: null, - linkId: 9, - linkType: 'page', - linkLabel: 'Contact us', - sectionId: null, - openNewWindow: false, - }, - }, - ], - }, - { - id: 'K-C_zRcH', - type: 'navbar_item', - settings: [ - { - id: 'link', - value: { - href: '/products', - text: 'Products #1', - email: null, - linkId: 'd870133f9a075477a96a58e7639d40c5', - linkType: 'page', - linkLabel: 'Products', - sectionId: null, - openNewWindow: true, - }, - }, - ], - }, - ], - settings: [ - { - id: 'logo', - value: { - id: 15, - url: 'http://localhost:3000/maglev/assets/15', - width: 572, - height: 290, - filename: 'Screen Shot 2021-06-30 at 3.44.04 PM.png', - byteSize: 35070, - }, - }, - ], - }, - ], locales: [ { label: 'English', prefix: 'en' }, { label: 'French', prefix: 'fr' }, diff --git a/app/frontend/editor/spec/__mocks__/theme.js b/app/frontend/editor/spec/__mocks__/theme.js index be98e3cc..3bfec906 100644 --- a/app/frontend/editor/spec/__mocks__/theme.js +++ b/app/frontend/editor/spec/__mocks__/theme.js @@ -515,4 +515,25 @@ export const theme = { 'icon-shop', 'icon-workshop', ], + layouts: [ + { + id: 'default', + label: 'Default', + groups: [ + { + id: 'header', + label: 'Header', + recoverable: ['navbar_01'] + }, + { + id: 'main', + label: 'Main' + }, + { + id: 'footer', + label: 'Footer' + } + ] + } + ] } diff --git a/app/frontend/editor/store/__tests__/getters.spec.js b/app/frontend/editor/store/__tests__/getters.spec.js index d2487673..3c774440 100644 --- a/app/frontend/editor/store/__tests__/getters.spec.js +++ b/app/frontend/editor/store/__tests__/getters.spec.js @@ -6,6 +6,7 @@ import buildGetters from '@/store/getters' import buildMutations from '@/store/mutations' import MockedServices from '@/spec/__mocks__/services' import { page, normalizedPage } from '@/spec/__mocks__/page' +import { sectionsContent, normalizedSectionsContent } from '@/spec/__mocks__/sections-content' import { site } from '@/spec/__mocks__/site' import { theme } from '@/spec/__mocks__/theme' @@ -31,33 +32,20 @@ describe('Getters', () => { describe('#content', () => { it('returns the content of the sections for the page', () => { - mockedServices.page.denormalize = vi.fn(() => page) + mockedServices.sectionsContent.denormalize = vi.fn(() => sectionsContent) expect( - store.getters.content.pageSections.map((section) => section.id), - ).toStrictEqual(['GrYZW-VP', '8hKSujtd', 'xM6f-kyh']) - expect( - store.getters.content.siteSections.map((section) => section.id), - ).toStrictEqual([]) - }) - it('returns the site content sections since they have been touched', () => { - store.commit('TOUCH_SECTION', 'GrYZW-VP') - mockedServices.page.denormalize = vi.fn(() => page) - expect( - store.getters.content.pageSections.map((section) => section.id), - ).toStrictEqual(['GrYZW-VP', '8hKSujtd', 'xM6f-kyh']) - expect( - store.getters.content.siteSections.map((section) => section.id), + store.getters.content[0].sections.map((section) => section.id), ).toStrictEqual(['GrYZW-VP']) + expect( + store.getters.content[1].sections.map((section) => section.id), + ).toStrictEqual(['8hKSujtd', 'xM6f-kyh']) }) }) describe('#defaultPageAttributes', () => { describe("Given the page hasn't been translated", () => { it('returns the title + path', () => { - const newNormalizedPage = { ...normalizedPage } - newNormalizedPage.entities.page['1'].translated = false - mockedServices.page.normalize = vi.fn(() => normalizedPage) - store.commit('SET_PAGE', page) + store.commit('SET_PAGE', { ...page, translated: false }) expect(store.getters.defaultPageAttributes).toStrictEqual({ title: 'Home page', path: 'index', @@ -66,38 +54,100 @@ describe('Getters', () => { }) describe('Given the page has been translated', () => { it('returns an empty object', () => { - const newNormalizedPage = { ...normalizedPage } - newNormalizedPage.entities.page['1'].translated = true - mockedServices.page.normalize = vi.fn(() => normalizedPage) - store.commit('SET_PAGE', page) + store.commit('SET_PAGE', { ...page, translated: true }) expect(store.getters.defaultPageAttributes).toStrictEqual({}) }) }) }) - describe('#sectionList', () => { - it('returns a list of objects (id, type, name, viewportFixedPosition)', () => { - mockedServices.page.denormalize = vi.fn(() => page) - expect(store.getters.sectionList).toStrictEqual([ + describe('#sectionsContent', () => { + it('returns the sections grouped by layout groups', () => { + store.commit('SET_PAGE', page) + mockedServices.sectionsContent.denormalize = vi.fn(() => sectionsContent) + expect(store.getters.sectionsContent).toStrictEqual([ { - id: 'GrYZW-VP', - type: 'navbar_01', - name: 'Navbar 01', - viewportFixedPosition: false, + label: 'Header', + id: 'header', + sections: [ + { + id: 'GrYZW-VP', + isMirrored: false, + mirroredPageTitle: undefined, + name: 'Navbar 01', + type: 'navbar_01', + viewportFixedPosition: false, + } + ], + lockVersion: 1 }, { - id: '8hKSujtd', - type: 'content_01', - name: 'Content #1', - viewportFixedPosition: false, + label: 'Main', + id: 'main', + sections: [ + { + id: '8hKSujtd', + isMirrored: false, + mirroredPageTitle: undefined, + name: 'Content #1', + type: 'content_01', + viewportFixedPosition: false, + }, + { + id: 'xM6f-kyh', + isMirrored: false, + mirroredPageTitle: undefined, + name: 'List #1', + type: 'list_01', + viewportFixedPosition: false + } + ], + lockVersion: 1 }, { - id: 'xM6f-kyh', - type: 'list_01', - name: 'List #1', - viewportFixedPosition: false, - }, + label: 'Footer', + id: 'footer', + sections: [], + lockVersion: 1 + } ]) }) }) + + describe('#canAddSection', () => { + beforeEach(() => { + mockedServices.sectionsContent.normalize = vi.fn(() => normalizedSectionsContent) + store.commit('SET_PAGE', page) + store.commit('SET_SECTIONS_CONTENT', sectionsContent) + }) + describe('the theme has many categories but without any sections', () => { + it('returns false', () => { + mockedServices.theme.buildCategories = vi.fn(() => ([ + { + label: 'Navbars', + children: [] + }, + { + label: 'Features', + children: [] + } + ])) + expect(store.getters.canAddSection('header')).toStrictEqual(false) + }) + }) + describe('the theme has many categories with sections', () => { + it('returns true', () => { + mockedServices.theme.buildCategories = vi.fn(() => ([ + { + label: 'Navbars', + children: [{ id: 'navbar_01' }, { id: 'navbar_02' }] + }, + { + label: 'Features', + children: [{ id: 'feature_01' }, { id: 'feature_02' }, { id: 'feature_03' }] + } + ])) + expect(store.getters.canAddSection('header')).toStrictEqual(true) + }) + }) + }) }) diff --git a/app/frontend/editor/store/actions/__tests__/page.spec.js b/app/frontend/editor/store/actions/__tests__/page.spec.js index 76c377c5..b9a61480 100644 --- a/app/frontend/editor/store/actions/__tests__/page.spec.js +++ b/app/frontend/editor/store/actions/__tests__/page.spec.js @@ -31,57 +31,6 @@ describe('Page Actions', () => { }) }) - describe('#persistPage', () => { - it('calls the API with both site and page attributes', async () => { - mockedServices.site.find = vi.fn(async () => site) - mockedServices.page.normalize = vi.fn(() => normalizedPage) - mockedServices.page.denormalize = vi.fn(() => page) - mockedServices.page.findById = vi.fn(async () => page) - mockedServices.page.update = vi.fn(async () => ({ - response: { status: 200 }, - })) - store.commit('SET_PAGE', page) - await store.dispatch('persistPage') - expect(mockedServices.page.update).toHaveBeenCalledWith( - 1, - { - sections: expect.any(Array), - lockVersion: 1, - }, - { - style: null, - }, - ) - expect(store.state.ui.saveButtonState).toEqual('success') - }) - it('calls the API without the site attributes because no site scoped section has been modified', async () => { - mockedServices.site.find = vi.fn(async () => site) - mockedServices.page.normalize = vi.fn(() => normalizedPage) - mockedServices.page.denormalize = vi.fn(() => page) - mockedServices.page.findById = vi.fn(async () => page) - mockedServices.page.update = vi.fn(async () => ({ - response: { status: 200 }, - })) - store.commit('SET_PAGE', page) - store.commit('TOUCH_SECTION', 'GrYZW-VP') - await store.dispatch('persistPage') - expect(mockedServices.page.update).toHaveBeenCalledWith( - 1, - { - sections: expect.any(Array), - lockVersion: 1, - }, - { - sections: expect.any(Array), - lockVersion: 1, - style: null, - }, - ) - expect(store.state.touchedSections).toEqual([]) - expect(store.state.ui.saveButtonState).toEqual('success') - }) - }) - describe('#setCurrentPageSettings', () => { it('does stuff', async () => { mockedServices.page.normalize = vi.fn(() => normalizedPage) diff --git a/app/frontend/editor/store/actions/__tests__/section-block.spec.js b/app/frontend/editor/store/actions/__tests__/section-block.spec.js index 1489ae97..0735a16e 100644 --- a/app/frontend/editor/store/actions/__tests__/section-block.spec.js +++ b/app/frontend/editor/store/actions/__tests__/section-block.spec.js @@ -13,6 +13,7 @@ import { } from '@/spec/__mocks__/section' import { site } from '@/spec/__mocks__/site' import { theme } from '@/spec/__mocks__/theme' +import { normalizedSectionsContent, sectionsContent, headerSections } from '@/spec/__mocks__/sections-content' const localVue = createLocalVue() localVue.use(Vuex) @@ -37,16 +38,16 @@ describe('SectionBlock Actions', () => { describe('#addSectionBlock', () => { it('marks the current section as touched', async () => { - mockedServices.page.normalize = vi.fn(() => normalizedPage) - mockedServices.page.denormalize = vi.fn(() => page) + mockedServices.sectionsContent.normalize = vi.fn(() => normalizedSectionsContent) + mockedServices.sectionsContent.denormalize = vi.fn(() => sectionsContent) mockedServices.section.normalize = vi.fn( () => normalizedNavContentSection, ) mockedServices.section.buildDefaultBlock = vi.fn( () => navContentSectionBlock, ) - store.commit('SET_PAGE', page) - store.commit('SET_SECTION', pageSections[0]) + store.commit('SET_SECTIONS_CONTENT', sectionsContent) + store.commit('SET_SECTION', headerSections[0]) await store.dispatch('addSectionBlock', { blockType: 'navbar_item', parentId: null, @@ -57,13 +58,13 @@ describe('SectionBlock Actions', () => { describe('#removeSectionBlock', () => { it('marks the current section as touched', async () => { - mockedServices.page.normalize = vi.fn(() => normalizedPage) - mockedServices.page.denormalize = vi.fn(() => page) + mockedServices.sectionsContent.normalize = vi.fn(() => normalizedSectionsContent) + mockedServices.sectionsContent.denormalize = vi.fn(() => sectionsContent) mockedServices.section.normalize = vi.fn( () => normalizedNavContentSection, ) - store.commit('SET_PAGE', page) - store.commit('SET_SECTION', pageSections[0]) + store.commit('SET_SECTIONS_CONTENT', sectionsContent) + store.commit('SET_SECTION', headerSections[0]) await store.dispatch('removeSectionBlock', 'RiEo8C3f') expect(store.state.touchedSections).toStrictEqual(['GrYZW-VP']) }) @@ -71,13 +72,13 @@ describe('SectionBlock Actions', () => { describe('#sortSectionBlocks', () => { it('marks the current section as touched', async () => { - mockedServices.page.normalize = vi.fn(() => normalizedPage) - mockedServices.page.denormalize = vi.fn(() => page) + mockedServices.sectionsContent.normalize = vi.fn(() => normalizedSectionsContent) + mockedServices.sectionsContent.denormalize = vi.fn(() => sectionsContent) mockedServices.section.normalize = vi.fn( () => normalizedNavContentSection, ) - store.commit('SET_PAGE', page) - store.commit('SET_SECTION', pageSections[0]) + store.commit('SET_SECTIONS_CONTENT', sectionsContent) + store.commit('SET_SECTION', headerSections[0]) await store.dispatch('sortSectionBlocks', [ { id: 'P1fGieWs' }, { id: 'RiEo8C3f' }, @@ -90,14 +91,14 @@ describe('SectionBlock Actions', () => { describe('#updateSectionBlockContent', () => { it('marks the current section as touched', async () => { - mockedServices.page.normalize = vi.fn(() => normalizedPage) - mockedServices.page.denormalize = vi.fn(() => page) + mockedServices.sectionsContent.normalize = vi.fn(() => normalizedSectionsContent) + mockedServices.sectionsContent.denormalize = vi.fn(() => sectionsContent) mockedServices.section.normalize = vi.fn( () => normalizedNavContentSection, ) - store.commit('SET_PAGE', page) - store.commit('SET_SECTION', pageSections[0]) - store.commit('SET_SECTION_BLOCK', pageSections[0].blocks[0]) + store.commit('SET_SECTIONS_CONTENT', sectionsContent) + store.commit('SET_SECTION', headerSections[0]) + store.commit('SET_SECTION_BLOCK', headerSections[0].blocks[0]) await store.dispatch('updateSectionBlockContent', {}) expect(store.state.touchedSections).toStrictEqual(['GrYZW-VP']) }) diff --git a/app/frontend/editor/store/actions/__tests__/section.spec.js b/app/frontend/editor/store/actions/__tests__/section.spec.js index b50f7269..c0a12ec6 100644 --- a/app/frontend/editor/store/actions/__tests__/section.spec.js +++ b/app/frontend/editor/store/actions/__tests__/section.spec.js @@ -6,13 +6,14 @@ import buildActions from '@/store/actions' import buildGetters from '@/store/getters' import buildMutations from '@/store/mutations' import MockedServices from '@/spec/__mocks__/services' -import { page, normalizedPage, pageSections } from '@/spec/__mocks__/page' import { simpleContentSection, normalizedSimpleContentSection, } from '@/spec/__mocks__/section' -import { site } from '@/spec/__mocks__/site' import { theme } from '@/spec/__mocks__/theme' +import { site } from '@/spec/__mocks__/site' +import { page } from '@/spec/__mocks__/page' +import { normalizedSectionsContent, sectionsContent, headerSections, mainSections } from '@/spec/__mocks__/sections-content' const localVue = createLocalVue() localVue.use(Vuex) @@ -36,27 +37,67 @@ describe('Section Actions', () => { }) describe('#addSection', () => { + beforeEach(() => { + mockedServices.sectionsContent.normalize = vi.fn(() => normalizedSectionsContent) + mockedServices.sectionsContent.denormalize = vi.fn(() => sectionsContent) + store.commit('SET_PAGE', page) + }) + it('marks the new section as touched', async () => { mockedServices.section.build = vi.fn(() => simpleContentSection) - mockedServices.page.normalize = vi.fn(() => normalizedPage) - mockedServices.page.denormalize = vi.fn(() => page) mockedServices.section.normalize = vi.fn( () => normalizedSimpleContentSection, ) - store.commit('SET_PAGE', page) + store.commit('SET_SECTIONS_CONTENT', sectionsContent) await store.dispatch('addSection', { + layoutGroupId: 'main', sectionDefinition: theme.sections[0], insertAt: 'bottom', }) expect(store.state.touchedSections).toStrictEqual(['NEW-CONTENT-1']) }) + + it('recovers a deleted section', async () => { + const altNormalizedSectionsContent = JSON.parse(JSON.stringify(normalizedSectionsContent)) + altNormalizedSectionsContent.entities.sections['GrYZW-VP'].deleted = true + mockedServices.sectionsContent.normalize = vi.fn(() => altNormalizedSectionsContent) + store.commit('SET_SECTIONS_CONTENT',sectionsContent) + await store.dispatch('addSection', { + layoutGroupId: 'header', + sectionDefinition: theme.sections[1] + }) + expect(store.state.sections['GrYZW-VP'].deleted).toEqual(false) + }) }) describe('#updateSectionContent', () => { it('marks the modified section as touched', async () => { - store.commit('SET_SECTION', pageSections[0]) + store.commit('SET_SECTION', headerSections[0]) await store.dispatch('updateSectionContent', {}) expect(store.state.touchedSections).toStrictEqual(['GrYZW-VP']) }) }) + + describe('#removeSection', () => { + beforeEach(() => { + mockedServices.sectionsContent.normalize = vi.fn(() => normalizedSectionsContent) + mockedServices.sectionsContent.denormalize = vi.fn(() => sectionsContent) + store.commit('SET_PAGE', page) + store.commit('SET_SECTIONS_CONTENT', sectionsContent) + }) + + it('removes the section from the layout group', () => { + store.dispatch('removeSection', mainSections[0].id) + expect(store.state.sections['GrYZW-VP'].deleted).toEqual(undefined) + expect(store.state.layoutGroups.main.sections).toStrictEqual(['xM6f-kyh']) + }) + + describe('the layout group mentions the section type as recoverable', () => { + it('flags the section as deleted', () => { + store.dispatch('removeSection', headerSections[0].id) + expect(store.state.sections['GrYZW-VP'].deleted).toEqual(true) + expect(store.state.layoutGroups.header.sections).toStrictEqual(['GrYZW-VP']) + }) + }) + }) }) diff --git a/app/frontend/editor/store/actions/index.js b/app/frontend/editor/store/actions/index.js index 7d8b867b..94696b2d 100644 --- a/app/frontend/editor/store/actions/index.js +++ b/app/frontend/editor/store/actions/index.js @@ -2,6 +2,7 @@ import buildSiteActions from './site' import buildPageActions from './page' import buildSectionActions from './section' import buildSectionBlockActions from './section-block' +import buildSectionsContentActions from './sections-content' export default (services) => ({ setDevice({ commit }, value) { @@ -31,4 +32,5 @@ export default (services) => ({ ...buildPageActions(services), ...buildSectionActions(services), ...buildSectionBlockActions(services), + ...buildSectionsContentActions(services), }) diff --git a/app/frontend/editor/store/actions/page.js b/app/frontend/editor/store/actions/page.js index e683e68d..b233ac64 100644 --- a/app/frontend/editor/store/actions/page.js +++ b/app/frontend/editor/store/actions/page.js @@ -3,63 +3,40 @@ import { isBlank } from '@/misc/utils' export default (services) => ({ // editPage : Action triggered when the user wants to edit another page // or to change the locale of the current page. - editPage({ state, dispatch }, { id, locale }) { - console.log('editPage', id, locale) - + async editPage({ state, dispatch }, { id, locale }) { // display the loader dispatch('resetPreview') - if (state.locale !== locale) { + if (locale && state.locale !== locale) { dispatch('setLocale', locale) - Promise.all([dispatch('fetchPage', id), dispatch('fetchSite')]) - } else dispatch('fetchPage', id) + await Promise.all([dispatch('fetchPage', id), dispatch('fetchSite')]) + } else await dispatch('fetchPage', id) + + dispatch('fetchSectionsContent', state.page.id) }, // Set page setPage({ commit }, page) { commit('SET_PAGE', page) }, + + // Set the oneSinglePage flag + setOneSinglePage({ commit }, oneSinglePage) { + commit('SET_ONE_SINGLE_PAGE', oneSinglePage) + }, + + // Reload a page: get fresh content + reload the preview iframe + async reloadPage({ state, dispatch }, { id }) { + await dispatch('editPage', { id }) + services.livePreview.reload() + }, + // Fetch a page from an id (or a path) async fetchPage({ commit, state: { site } }, id) { return services.page .findById(site, id) .then((page) => commit('SET_PAGE', page)) - }, - // Persist the content of a page (including or not the site content) - async persistPage({ - commit, - dispatch, - state: { page, site, style }, - getters: { content, defaultPageAttributes }, - }) { - commit('SET_SAVE_BUTTON_STATE', 'inProgress') - - const pageAttributes = { - sections: content.pageSections, - lockVersion: page.lockVersion, - ...defaultPageAttributes, - } - const siteAttributes = isBlank(content.siteSections) - ? { style } - : { - sections: content.siteSections, - lockVersion: site.lockVersion, - style, - } - - return services.page - .update(page.id, pageAttributes, siteAttributes) - .then(() => { - commit('SET_SAVE_BUTTON_STATE', 'success') - commit('RESET_TOUCHED_SECTIONS') - Promise.all([dispatch('fetchPage', page.id), dispatch('fetchSite')]) - }) - .catch(({ response: { status } }) => { - commit('SET_SAVE_BUTTON_STATE', 'fail') - console.log('[Maglev] could not save the page', status) - if (status === 409) commit('OPEN_ERROR_MODAL', 'staleRecord') - }) - }, + }, setCurrentPageSettings({ commit }, pageSettings) { commit('SET_PAGE_SETTINGS', pageSettings) }, diff --git a/app/frontend/editor/store/actions/section-block.js b/app/frontend/editor/store/actions/section-block.js index ec20f217..a07b4760 100644 --- a/app/frontend/editor/store/actions/section-block.js +++ b/app/frontend/editor/store/actions/section-block.js @@ -21,7 +21,9 @@ export default (services) => ({ if (parentId) sectionBlock.parentId = parentId commit('ADD_SECTION_BLOCK', sectionBlock) commit('TOUCH_SECTION', section.id) + services.livePreview.addBlock( + getters.sectionLayoutGroupIdMap[section.id], getters.content, getters.denormalizedSection, sectionBlock, @@ -30,7 +32,9 @@ export default (services) => ({ removeSectionBlock({ commit, getters, state: { section } }, id) { commit('REMOVE_SECTION_BLOCK', id) commit('TOUCH_SECTION', section.id) + services.livePreview.removeBlock( + getters.sectionLayoutGroupIdMap[section.id], getters.content, getters.denormalizedSection, id, @@ -39,7 +43,12 @@ export default (services) => ({ sortSectionBlocks({ commit, getters, state: { section } }, change) { commit('SORT_SECTION_BLOCKS', change) commit('TOUCH_SECTION', section.id) - services.livePreview.moveBlock(getters.content, getters.denormalizedSection) + + services.livePreview.moveBlock( + getters.sectionLayoutGroupIdMap[section.id], + getters.content, + getters.denormalizedSection + ) }, updateSectionBlockContent( { commit, getters, state: { section, sectionBlock } }, @@ -47,7 +56,9 @@ export default (services) => ({ ) { commit('UPDATE_SECTION_BLOCK_CONTENT', change) commit('TOUCH_SECTION', section.id) + services.livePreview.updateBlock( + getters.sectionLayoutGroupIdMap[section.id], getters.content, getters.denormalizedSection, sectionBlock, diff --git a/app/frontend/editor/store/actions/section.js b/app/frontend/editor/store/actions/section.js index 93aa61e9..b74dd093 100644 --- a/app/frontend/editor/store/actions/section.js +++ b/app/frontend/editor/store/actions/section.js @@ -13,42 +13,103 @@ export default (services) => ({ commit('SET_HOVERED_SECTION', null) }, addSection( - { commit, getters, state: { site } }, - { sectionDefinition, insertAt }, + { commit, getters, state: { siteScopedSections } }, + { layoutGroupId, sectionDefinition, insertAt }, ) { - if (sectionDefinition.insertAt) insertAt = sectionDefinition.insertAt - const section = services.section.build(sectionDefinition, site) - commit('ADD_SECTION', { section, insertAt }) + let section = undefined + const deletedSection = getters.deletedSection(layoutGroupId, sectionDefinition.id) + + if (deletedSection) { + commit('RESTORE_SECTION', deletedSection.id) + section = getters.denormalizeSection(deletedSection.id) + } else { + if (sectionDefinition.insertAt) insertAt = sectionDefinition.insertAt + section = services.section.build(sectionDefinition, siteScopedSections) + commit('ADD_SECTION', { layoutGroupId, section, insertAt }) + } + commit('TOUCH_SECTION', section.id) - services.livePreview.addSection(getters.content, section, insertAt) + + services.livePreview.addSection(layoutGroupId, getters.content, section, insertAt) + return section }, - removeSection({ commit }, sectionId) { - commit('REMOVE_SECTION', sectionId) - services.livePreview.removeSection(sectionId) + removeSection({ commit, getters, state }, sectionId) { + const layoutGroupId = getters.sectionLayoutGroupIdMap[sectionId] + const recoverable = getters.layoutGroupDefinition(layoutGroupId).recoverable ?? [] + commit('REMOVE_SECTION', { layoutGroupId, sectionId, recoverable }) + services.livePreview.removeSection(layoutGroupId, sectionId) }, updateSectionContent({ commit, getters, state: { section } }, change) { commit('UPDATE_SECTION_CONTENT', change) commit('TOUCH_SECTION', section.id) + services.livePreview.updateSection( + getters.sectionLayoutGroupIdMap[section.id], getters.content, getters.denormalizedSection, change, ) }, + // source: the information related to the section we want to mirror in the current page + // target: the information about where to put the new section (layoutGroupId, insertAt) + async addMirroredSection( + { commit, getters }, + { source, target: { layoutGroupId, insertAt } }, + ) { + // get the content of the section we want to mirror + const section = await services.sectionsContent.findSingleSection(source.pageId, source.layoutGroupId, source.sectionId) + + section.mirrorOf = { enabled: true, ...source } + + commit('ADD_SECTION', { layoutGroupId, section, insertAt }) + commit('TOUCH_SECTION', section.id) + + services.livePreview.addSection(layoutGroupId, getters.content, section, insertAt) + + return section + }, + async toggleMirroredSectionEnabled({ commit, dispatch }, enabled) { + commit('SET_SECTION_MIRROR_OF_ENABLED', enabled) + + if (!enabled) return + + // update the content of the current section with the one from the mirror + dispatch('updateMirrorOfSectionContent') + }, + async updateMirrorOfSectionContent({ state, commit, getters }) { + const mirrorOf = state.section.mirrorOf + const section = await services.sectionsContent.findSingleSection(mirrorOf.pageId, mirrorOf.layoutGroupId, mirrorOf.sectionId) + + section.mirrorOf = { enabled: true, ...mirrorOf } + + commit('SET_SECTION', section) // required to update the current section form + commit('SET_SECTION_CONTENT', section) // required to prepare the update of the preview iframe + commit('TOUCH_SECTION', state.section.id) + + services.livePreview.updateSection( + getters.sectionLayoutGroupIdMap[state.section.id], + getters.content, + getters.denormalizedSection, + {}, + ) + }, moveSection( { commit, getters, - state: { - page: { sections }, - }, + state: { layoutGroups }, }, - { from, to }, + { layoutGroupId, from, to }, ) { if (isBlank(from) || isBlank(to)) return - commit('MOVE_HOVERED_SECTION', { fromIndex: from, toIndex: to }) + + const sections = layoutGroups[layoutGroupId].sections + + commit('MOVE_HOVERED_SECTION', { layoutGroupId, fromIndex: from, toIndex: to }) + services.livePreview.moveSection( + layoutGroupId, getters.content, sections[from], sections[to], @@ -60,16 +121,19 @@ export default (services) => ({ dispatch, state: { hoveredSection: { sectionId }, - page: { sections }, + layoutGroups, }, + getters }, direction, ) { + const layoutGroupId = getters.sectionLayoutGroupIdMap[sectionId] + const sections = layoutGroups[layoutGroupId].sections const indices = services.section.calculateMovingIndices( sections, sectionId, direction, - ) - dispatch('moveSection', { from: indices.fromIndex, to: indices.toIndex }) - }, + ) + dispatch('moveSection', { layoutGroupId, from: indices.fromIndex, to: indices.toIndex }) + } }) diff --git a/app/frontend/editor/store/actions/sections-content.js b/app/frontend/editor/store/actions/sections-content.js new file mode 100644 index 00000000..11fdc376 --- /dev/null +++ b/app/frontend/editor/store/actions/sections-content.js @@ -0,0 +1,37 @@ +import { isBlank } from '@/misc/utils' + +export default (services) => ({ + fetchSectionsContent({ commit }, pageId) { + services.sectionsContent.find(pageId) + .then((content) => commit('SET_SECTIONS_CONTENT', content)) + }, + setSectionsContent({ commit }, content) { + commit('SET_SECTIONS_CONTENT', content) + }, + setSiteScopedSections({ commit }, sections) { + commit('SET_SITE_SCOPED_SECTIONS', sections) + }, + // Persist the "content" of a page + async persistSectionsContent({ + commit, + dispatch, + state, + getters: { content, defaultPageAttributes }, + }) { + commit('SET_SAVE_BUTTON_STATE', 'inProgress') + + return services.sectionsContent + .update(state.page.id, content) + .then(({ lockVersions }) => { + commit('SET_SAVE_BUTTON_STATE', 'success') + commit('RESET_TOUCHED_SECTIONS') + + commit('SET_SECTIONS_CONTENT_LOCK_VERSIONS', lockVersions) + }) + .catch(({ response: { status } }) => { + commit('SET_SAVE_BUTTON_STATE', 'fail') + console.log('[Maglev] could not save the content', status) + if (status === 409) commit('OPEN_ERROR_MODAL', 'staleRecord') + }) + }, +}) diff --git a/app/frontend/editor/store/actions/site.js b/app/frontend/editor/store/actions/site.js index 04ad084f..afa9221e 100644 --- a/app/frontend/editor/store/actions/site.js +++ b/app/frontend/editor/store/actions/site.js @@ -5,6 +5,7 @@ export default (services) => ({ services.api.setSiteHandle(site.handle) commit('SET_SITE', rawSite) commit('SET_STYLE', style) + commit('SET_ONE_SINGLE_PAGE', site.numberOfPages === 1) }) }, loadPublishButtonState({ state, commit }) { diff --git a/app/frontend/editor/store/default-state.js b/app/frontend/editor/store/default-state.js index bc1590fd..fc3359a9 100644 --- a/app/frontend/editor/store/default-state.js +++ b/app/frontend/editor/store/default-state.js @@ -11,10 +11,14 @@ export default { hoveredSection: null, sectionBlock: null, sectionBlockDefinition: null, + sectionsContent: [], // array of layoutGroups + layoutGroups: {}, // each layoutGroup has a list of sections sections: {}, sectionBlocks: {}, + siteScopedSections: {}, // site-wide sections like headers/footers editorSettings: {}, touchedSections: [], + oneSinglePage: true, ui: { saveButtonState: 'default', publishButtonState: { diff --git a/app/frontend/editor/store/getters.js b/app/frontend/editor/store/getters.js index c6ccd81d..e92895ad 100644 --- a/app/frontend/editor/store/getters.js +++ b/app/frontend/editor/store/getters.js @@ -1,60 +1,131 @@ + import { isBlank } from '@/misc/utils.js' + export default (services) => ({ - sectionList: ( - { page, sections, sectionBlocks }, - { sectionDefinition: getSectiondefinition }, + // we need it to build the "Organize sections" pane + sectionsContent: ( + { sectionsContent, layoutGroups, sections, sectionBlocks }, + { sectionDefinition: getSectiondefinition, layoutGroupDefinition: getLayoutGroupDefinition }, ) => { - const pageContent = services.page.denormalize(page, { + const content = services.sectionsContent.denormalize(sectionsContent, { + layoutGroups, sections, - blocks: sectionBlocks, + blocks: sectionBlocks }) - if (!pageContent?.sections) return [] - return pageContent.sections.map((sectionContent) => { - const sectionDefinition = getSectiondefinition(sectionContent) + + return content.map(layoutGroup => { + const layoutDefinition = getLayoutGroupDefinition(layoutGroup.id) return { - id: sectionContent.id, - type: sectionContent['type'], - name: sectionDefinition.name, - viewportFixedPosition: !!sectionDefinition.viewportFixedPosition, + label: layoutDefinition.label, + ...layoutGroup, + sections: layoutGroup.sections.map(sectionContent => { + if (sectionContent.deleted) return + const sectionDefinition = getSectiondefinition(sectionContent) + return { + id: sectionContent.id, + type: sectionContent['type'], + name: sectionDefinition.name, + isMirrored: sectionContent.mirrorOf?.enabled ?? false, + mirroredPageTitle: sectionContent.mirrorOf?.pageTitle, + viewportFixedPosition: !!sectionDefinition.viewportFixedPosition, + } + }).filter(content => content) } }) }, - stickySectionList: (_, { sectionList }) => { - return sectionList.filter((section) => section.viewportFixedPosition) + + categoriesByLayoutGroupId: + ({ theme, page: { layoutId } }, { sectionsByLayoutGroupId }) => + (layoutGroupId) => { + const insertedSectionTypes = sectionsByLayoutGroupId(layoutGroupId).map(section => { + return isBlank(section.deleted) || section.deleted === false ? section.type : undefined + }).filter(section => section) + return services.theme.buildCategories({ + theme, + layoutId, + layoutGroupId, + insertedSectionTypes + }) + }, + + // return all the section types in the page + sectionTypes: ({ sections }) => { + return Object.values(sections).map((section) => section.type) + }, + stickySectionList: ({ sections }, { sectionDefinition: getSectiondefinition }) => { + return Object.values(sections).filter((sectionContent) => { + const sectionDefinition = getSectiondefinition(sectionContent) + return !!sectionDefinition.viewportFixedPosition + }) }, defaultPageAttributes: ({ page }) => { if (page.translated) return {} return { title: page.title, path: page.path } }, content: ( - { page, sections, sectionBlocks, touchedSections }, - { sectionDefinition: getSectiondefinition }, + { sectionsContent, layoutGroups, sections, sectionBlocks, touchedSections } ) => { - const pageContent = services.page.denormalize(page, { + return services.sectionsContent.denormalize(sectionsContent, { + layoutGroups, sections, blocks: sectionBlocks, }) - - const siteSections = pageContent.sections.filter( - (sectionContent) => getSectiondefinition(sectionContent).siteScoped, - ) - const hasModifiedSiteScopedSections = siteSections.some( - (sectionContent) => touchedSections.indexOf(sectionContent.id) !== -1, - ) - return { - pageSections: pageContent.sections, - siteSections: hasModifiedSiteScopedSections ? siteSections : [], - } }, - denormalizedSection: ({ page, sections, sectionBlocks, section }) => { - const pageContent = services.page.denormalize(page, { - sections, - blocks: sectionBlocks, - }) - return pageContent.sections.find((s) => s.id == section.id) + // denormalize the current section in the state + denormalizedSection: ({ section: { id: sectionId } }, { denormalizeSection }) => { + return denormalizeSection(sectionId) + }, + denormalizeSection: ({}, { content }) => + (sectionId) => { + for (const layoutGroupId in content) { + const sections = content[layoutGroupId].sections + const section = sections.find(s => s.id === sectionId) + if (section) return section + } + return null }, sectionContent: ({ section }) => { return section ? [...section.settings] : null }, + layoutDefinition: + ({ theme, page }) => { + return theme.layouts.find(layout => layout.id === page.layoutId) + }, + layoutGroupDefinition: + ({}, { layoutDefinition }) => + (layoutGroupId) => { + return layoutDefinition.groups.find(group => group.id === layoutGroupId) + }, + sectionLayoutGroupIdMap: + ({ layoutGroups }) => { + const memo = {} + for (const layoutGroupId in layoutGroups) { + layoutGroups[layoutGroupId].sections.forEach(sectionId => { + memo[sectionId] = layoutGroupId + }) + } + return memo + }, + sectionsByLayoutGroupId: + ({ layoutGroups, sections }) => + (layoutGroupId) => { + for (const groupId in layoutGroups) { + if (layoutGroupId !== groupId) continue + const layoutGroup = layoutGroups[groupId] + return layoutGroup.sections.map(sectionId => sections[sectionId]) + } + return [] + }, + deletedSection: + ({}, { layoutGroupDefinition, sectionsByLayoutGroupId }) => + (layoutGroupId, type) => { + const recoverable = layoutGroupDefinition(layoutGroupId).recoverable + + // if the section isn't recoverable, no need to get further + if (isBlank(recoverable) || recoverable.indexOf(type) === -1) return undefined + + const sections = sectionsByLayoutGroupId(layoutGroupId) + return sections.find(section => section.type === type && section.deleted) + }, sectionDefinition: ({ theme }) => (sectionContent) => { @@ -88,7 +159,6 @@ export default (services) => ({ return services.section.getBlockLabel(sectionBlock, definition, index) }, sectionBlockIndex: ({ section, sectionBlock }) => { - // console.log(section.blocks, sectionBlock) return sectionBlock ? section.blocks.indexOf(sectionBlock.id) + 1 : null }, sectionBlockContent: ({ sectionBlock }) => { @@ -99,4 +169,25 @@ export default (services) => ({ (advanced) => { return services.section.getSettings(sectionBlockDefinition, advanced) }, + canAddSection: + ({}, { categoriesByLayoutGroupId }) => + (layoutGroupId) => { + const categories = categoriesByLayoutGroupId(layoutGroupId) + return categories.some(({ children }) => children.length > 0) + }, + canAddMirroredSection: ({ theme, oneSinglePage }, { layoutGroupDefinition }) => + (layoutGroupId) => { + const layoutGroup = layoutGroupDefinition(layoutGroupId) + return theme.mirrorSection && layoutGroup.mirrorSection !== false && services.section.canAddMirroredSection({ + hasOneSinglePage: oneSinglePage + }) + }, + isMirroredSection: ({ section }) => { + return !isBlank(section.mirrorOf) + }, + isMirroredSectionEditable: ({ theme, section, page }, { isMirroredSection }) => { + if (!isMirroredSection) return false + if (section.mirrorOf.enabled === false) return true + return theme.mirrorSection === true || (theme.mirrorSection === 'protected' && section.mirrorOf?.pageId === page.id) + } }) diff --git a/app/frontend/editor/store/index.js b/app/frontend/editor/store/index.js index 585a24e9..da18b02a 100644 --- a/app/frontend/editor/store/index.js +++ b/app/frontend/editor/store/index.js @@ -22,6 +22,8 @@ store.dispatch('fetchSite', true) store.dispatch('setTheme', window.theme) store.dispatch('setPage', window.page) store.dispatch('setLocale', window.locale) +store.dispatch('setSectionsContent', window.sectionsContent) +store.dispatch('setSiteScopedSections', window.siteScopedSections) if (store.state.editorSettings.sitePublishable) store.dispatch('pollLastPublication') diff --git a/app/frontend/editor/store/mutations.js b/app/frontend/editor/store/mutations.js index a5e70eb6..323983dd 100644 --- a/app/frontend/editor/store/mutations.js +++ b/app/frontend/editor/store/mutations.js @@ -31,17 +31,44 @@ export default (services) => ({ state.theme = theme }, SET_PAGE(state, page) { - const { entities } = services.page.normalize(page) - state.page = entities.page[page.id] - state.sections = { ...state.sections, ...entities.sections } - state.sectionBlocks = { ...state.sectionBlocks, ...entities.blocks } - state.hoveredSection = null + state.page = page }, SET_PAGE_SETTINGS(state, page) { const attributes = pick(page, ...PAGE_SETTING_ATTRIBUTES) omitEmpty(attributes) state.page = { ...state.page, ...attributes } }, + SET_ONE_SINGLE_PAGE(state, oneSinglePage) { + state.oneSinglePage = oneSinglePage + }, + // === SECTIONS CONTENT === + SET_SECTIONS_CONTENT(state, content) { + const { entities, result } = services.sectionsContent.normalize(content) + + state.sectionsContent = [...result] + state.layoutGroups = { ...state.layoutGroups, ...entities.layoutGroups } + state.sections = { ...state.sections, ...entities.sections } + state.sectionBlocks = { ...state.sectionBlocks, ...entities.blocks } + state.hoveredSection = null + }, + SET_SITE_SCOPED_SECTIONS(state, sections) { + state.siteScopedSections = { ...sections } + }, + SET_SECTION_CONTENT(state, content) { + const { entities, result } = services.section.normalize(content) + + state.sections = { ...state.sections, ...entities.sections } + state.sectionBlocks = { ...state.sectionBlocks, ...entities.blocks } + }, + SET_SECTIONS_CONTENT_LOCK_VERSIONS(state, lockVersions) { + for (const { layoutGroupId, lockVersion } of lockVersions) { + state.layoutGroups[layoutGroupId] = { + ...state.layoutGroups[layoutGroupId], + lockVersion + } + } + }, + // === SECTION === SET_SECTION(state, section) { if (section) { const sectionDefinition = state.theme.sections.find( @@ -79,39 +106,62 @@ export default (services) => ({ state.sections[state.section.id] = updatedSection state.section = updatedSection }, - ADD_SECTION(state, { section, insertAt }) { + SET_SECTION_MIRROR_OF_ENABLED(state, enabled) { + let updatedSection = { ...state.section } + updatedSection.mirrorOf.enabled = enabled + + state.sections[state.section.id] = updatedSection + state.section = updatedSection + }, + ADD_SECTION(state, { layoutGroupId, section, insertAt }) { const { entities: { sections, blocks }, } = services.section.normalize(section) state.sections = { ...state.sections, [section.id]: sections[section.id] } state.sectionBlocks = { ...state.sectionBlocks, ...blocks } // hmmm??? - const updatedPage = { ...state.page } + const layoutGroup = { ...state.layoutGroups[layoutGroupId] } + // use a new mem reference for the list of sections + layoutGroup.sections = [...layoutGroup.sections] + switch (insertAt) { case 'top': - updatedPage.sections.unshift(section.id) + layoutGroup.sections.unshift(section.id) break case 'bottom': case undefined: case null: case '': - updatedPage.sections.push(section.id) + layoutGroup.sections.push(section.id) break default: - updatedPage.sections.splice( - updatedPage.sections.indexOf(insertAt) + 1, + layoutGroup.sections.splice( + layoutGroup.sections.indexOf(insertAt) + 1, 0, section.id, ) } - state.page = updatedPage - }, - REMOVE_SECTION(state, sectionId) { - state.page.sections.splice(state.page.sections.indexOf(sectionId), 1) + state.layoutGroups[layoutGroupId] = layoutGroup + }, + RESTORE_SECTION(state, sectionId) { + const section = state.sections[sectionId] + state.sections[sectionId] = { ...section, deleted: false } + }, + REMOVE_SECTION(state, { layoutGroupId, sectionId, recoverable }) { + const section = state.sections[sectionId] + + if (recoverable.indexOf(section.type) !== -1) { + // soft delete the section + state.sections[sectionId] = { ...section, deleted: true } + } else { + const sections = state.layoutGroups[layoutGroupId].sections + sections.splice(sections.indexOf(sectionId), 1) + } }, - MOVE_HOVERED_SECTION(state, { fromIndex, toIndex }) { - state.page.sections = arraymove(state.page.sections, fromIndex, toIndex) + MOVE_HOVERED_SECTION(state, { layoutGroupId, fromIndex, toIndex }) { + const layoutGroup = state.layoutGroups[layoutGroupId] + layoutGroup.sections = arraymove(layoutGroup.sections, fromIndex, toIndex) }, SET_SECTION_BLOCK(state, sectionBlock) { state.sectionBlock = sectionBlock diff --git a/app/frontend/editor/views/content-pane.vue b/app/frontend/editor/views/content-pane.vue index be755408..2fd50d79 100644 --- a/app/frontend/editor/views/content-pane.vue +++ b/app/frontend/editor/views/content-pane.vue @@ -4,6 +4,7 @@ :overflowY="false" :max-width-pane="isMaxWidthPane" :with-pre-title="isSectionBlockVersion" + :with-custom-title="isMirroredSection" >@@ -21,6 +22,20 @@
+ +NoCoffee, passionated developers,
creators of web applications, mobiles apps and
fancy R&D projects.
NoCoffee, passionated developers,
creators of web applications, mobiles apps and
fancy R&D projects.