maglevcms 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/maglev/tailwind.css +3 -27
  3. data/app/assets/javascripts/maglev/client/dom-operations.js +9 -1
  4. data/app/assets/javascripts/maglev/editor/controllers/app/forms/section_form_controller.js +31 -6
  5. data/app/assets/javascripts/maglev/editor/controllers/app/page_preview_controller.js +20 -6
  6. data/app/assets/javascripts/maglev/editor/controllers/app/preview_notification_center_controller.js +10 -4
  7. data/app/components/maglev/uikit/app_layout/topbar/page_info_component.html.erb +5 -5
  8. data/app/controllers/concerns/maglev/editor/errors_concern.rb +2 -1
  9. data/app/controllers/concerns/maglev/editor/lock_version_concern.rb +27 -0
  10. data/app/controllers/maglev/editor/section_blocks_controller.rb +3 -9
  11. data/app/controllers/maglev/editor/sections_controller.rb +9 -15
  12. data/app/controllers/maglev/studio/section_preview_controller.rb +76 -0
  13. data/app/helpers/maglev/application_helper.rb +34 -0
  14. data/app/helpers/maglev/studio/section_preview_helper.rb +9 -0
  15. data/app/models/concerns/maglev/sections_concern.rb +0 -6
  16. data/app/services/concerns/maglev/content/helpers_concern.rb +1 -1
  17. data/app/services/maglev/app_container.rb +2 -0
  18. data/app/services/maglev/content/sort_sections_service.rb +67 -0
  19. data/app/services/maglev/fetch_section_screenshot_path.rb +11 -1
  20. data/app/views/maglev/editor/assets/_list.html.erb +0 -1
  21. data/app/views/maglev/editor/assets/index.html.erb +4 -1
  22. data/config/routes/maglev/studio.rb +6 -0
  23. data/config/routes.rb +3 -0
  24. data/lib/generators/maglev/templates/theme/app/views/theme/layout.html.erb.tt +1 -1
  25. data/lib/maglev/version.rb +1 -1
  26. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3061afa4c3b8ce916e981ea025e8db6b63102287b152163369813ec9ac172b5e
4
- data.tar.gz: 484f90090316bd4a319e4223b5d86d5c673260f56808386f2f792a1b12613331
3
+ metadata.gz: 91a5ea2feecec6351e3e7042be72418ce8e95374e50108d6a798b2f179d47ee4
4
+ data.tar.gz: c674bf2fb233c7aec70823875c804e7f7741df83f809a09cc670ac13eedb9d74
5
5
  SHA512:
6
- metadata.gz: 2c2df97bdeee5ff72c3c2fb7796e3d14b23169a71c68bb91879a002aa944e678f8fe1461ada1b3512394bf5f4ba848c35636345023026549fe15c964527d28ea
7
- data.tar.gz: 56665addc22ceb997563182a3217ebfc6fcfdefa9a619ea34ed6e1dcdbe3c7a3fc5aef6f14c041df2e081cbc78f9abb11bd7f3121ee21681c62f5b16b8309b5e
6
+ metadata.gz: 2f3301ae182676801e8fc2baecbc6774f1c3e10e072fa509a2272185ff6b62aa64c7cdd68a92426679b6545c14a40f376e0be3f038df1cb18d78cd6e55747da1
7
+ data.tar.gz: a1dce55e8a53f13e394b82f548a6726a06b2a3babe65e6e645d1ee5c00040022d10243f75c9eede599fe957e4b2bb0aafe5fd8a9becc32dfa8b35a3f4767a873
@@ -55,9 +55,7 @@
55
55
  --container-xl: 36rem;
56
56
  --container-2xl: 42rem;
57
57
  --container-3xl: 48rem;
58
- --container-4xl: 56rem;
59
58
  --container-5xl: 64rem;
60
- --container-6xl: 72rem;
61
59
  --container-7xl: 80rem;
62
60
  --text-xs: 0.75rem;
63
61
  --text-xs--line-height: calc(1 / 0.75);
@@ -739,6 +737,9 @@
739
737
  .max-w-xs {
740
738
  max-width: var(--container-xs);
741
739
  }
740
+ .min-w-0 {
741
+ min-width: calc(var(--spacing) * 0);
742
+ }
742
743
  .min-w-64 {
743
744
  min-width: calc(var(--spacing) * 64);
744
745
  }
@@ -2717,36 +2718,16 @@
2717
2718
  display: flex;
2718
2719
  }
2719
2720
  }
2720
- .lg\:w-2xl {
2721
- @media (width >= 64rem) {
2722
- width: var(--container-2xl);
2723
- }
2724
- }
2725
2721
  .lg\:w-3xl {
2726
2722
  @media (width >= 64rem) {
2727
2723
  width: var(--container-3xl);
2728
2724
  }
2729
2725
  }
2730
- .lg\:w-4xl {
2731
- @media (width >= 64rem) {
2732
- width: var(--container-4xl);
2733
- }
2734
- }
2735
- .lg\:w-5xl {
2736
- @media (width >= 64rem) {
2737
- width: var(--container-5xl);
2738
- }
2739
- }
2740
2726
  .lg\:grid-cols-2 {
2741
2727
  @media (width >= 64rem) {
2742
2728
  grid-template-columns: repeat(2, minmax(0, 1fr));
2743
2729
  }
2744
2730
  }
2745
- .lg\:grid-cols-3 {
2746
- @media (width >= 64rem) {
2747
- grid-template-columns: repeat(3, minmax(0, 1fr));
2748
- }
2749
- }
2750
2731
  .lg\:space-x-32 {
2751
2732
  @media (width >= 64rem) {
2752
2733
  :where(& > :not(:last-child)) {
@@ -2787,11 +2768,6 @@
2787
2768
  width: var(--container-5xl);
2788
2769
  }
2789
2770
  }
2790
- .xl\:w-6xl {
2791
- @media (width >= 80rem) {
2792
- width: var(--container-6xl);
2793
- }
2794
- }
2795
2771
  .xl\:grid-cols-3 {
2796
2772
  @media (width >= 80rem) {
2797
2773
  grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -35,7 +35,13 @@ export const start = () => {
35
35
 
36
36
  const addSection = (event) => {
37
37
  const { sectionId, insertAt } = event.detail
38
- debouncedUpdatePreviewDocument({ sectionId, insertAt, scrollToSection: true })
38
+
39
+ // Do not use debouncedUpdatePreviewDocument: it is shared with section:update and block
40
+ // refreshes. A follow-up call within the debounce window only passes { sectionId }, which
41
+ // replaces this invocation and drops insertAt — the new section is then appended.
42
+ void updatePreviewDocument({ sectionId, insertAt, scrollToSection: true }).catch((err) => {
43
+ log('[DOM Operations] addSection failed', err)
44
+ })
39
45
  }
40
46
 
41
47
  const moveSections = (event) => {
@@ -221,6 +227,8 @@ const updatePreviewDocument = async ({ sectionId, insertAt = undefined, scrollTo
221
227
  scrollToSectionOrBlock(targetElement)
222
228
  }
223
229
 
230
+ // Shared debouncer: last invocation within 150ms wins. A later call with a narrower payload
231
+ // (e.g. only { sectionId }) replaces a pending one and drops extra args — see addSection().
224
232
  const debouncedUpdatePreviewDocument = debounce(updatePreviewDocument, 150)
225
233
 
226
234
  const insertSectionInDOM = (element, insertAt) => {
@@ -9,8 +9,10 @@ export default class extends Controller {
9
9
  static debounces = ['afterSettingUpdate']
10
10
 
11
11
  connect() {
12
- useDebounce(this)
12
+ useDebounce(this)
13
+
13
14
  this.sourceId = this.sectionBlockIdValue !== '' ? this.sectionBlockIdValue : this.sectionIdValue
15
+
14
16
  requestAnimationFrame(() => {
15
17
  this.dispatch('connected', { bubbles: true, detail: {
16
18
  sectionId: this.sectionIdValue,
@@ -18,6 +20,12 @@ export default class extends Controller {
18
20
  lockVersion: this.sectionLockVersionValue }
19
21
  })
20
22
  })
23
+
24
+ // we need to keep track of the submitting state to avoid multiple requests being sent with a wrong lock version
25
+ this.submitting = false
26
+ this.element.addEventListener('turbo:submit-end', (event) => {
27
+ if (!event.detail.success) this.submitting = false
28
+ })
21
29
  }
22
30
 
23
31
  onSettingChange(event) {
@@ -33,15 +41,32 @@ export default class extends Controller {
33
41
  onPersist(event) {
34
42
  log('[SectionForm::onPersist]', event.detail, typeof event.detail)
35
43
  this.lockVersionTarget.value = event.detail.lockVersion
44
+
45
+ // we are done submitting, so we can reset the submitting flag
46
+ this.submitting = false
36
47
  }
37
48
 
38
- async afterSettingUpdate(event) {
49
+ afterSettingUpdate(event) {
50
+ // if the request is still running (flight), we need to debounce the call
51
+ // to avoid multiple requests being sent with a wrong lock version
52
+ if (this.submitting) {
53
+ this.afterSettingUpdate(event)
54
+ return
55
+ }
56
+
39
57
  log('[SectionForm::afterSettingUpdate] 🙌🙌', event.detail)
40
- await this.element.requestSubmit()
41
-
58
+
59
+ // we couldn't update the content just by modifying the DOM, so we have to refresh the whole section.
60
+ // The right time to do this is when the request is finished, so we listen for the turbo:submit-end event.
42
61
  if (!event.detail.updated) {
43
- // we couldn't update the content just by modifying the DOM, so we have to refresh the whole section
44
- this.dispatch('updateSection', { detail: { sectionId: this.sectionIdValue } })
62
+ this.element.addEventListener('turbo:submit-end', () => {
63
+ this.dispatch('updateSection', { detail: { sectionId: this.sectionIdValue } })
64
+ }, { once: true })
45
65
  }
66
+
67
+ // better to set it to true instead of relying on the turbo:submit-start event
68
+ this.submitting = true
69
+
70
+ this.element.requestSubmit()
46
71
  }
47
72
  }
@@ -164,13 +164,14 @@ export default class extends Controller {
164
164
 
165
165
  setupTransformations() {
166
166
  this.calculateTransformations = this.calculateTransformations.bind(this)
167
- this.ro = new ResizeObserver(this.calculateTransformations)
168
-
167
+ this.scheduleTransformations = this.scheduleTransformations.bind(this)
168
+ this.resizeObserver = new ResizeObserver(this.scheduleTransformations)
169
+
169
170
  const sidebar = this.appLayoutSidebar
170
171
  const pageLayout = document.querySelector('#page-layout')
171
- if (sidebar) this.ro.observe(this.appLayoutSidebar())
172
- if (pageLayout) this.ro.observe(pageLayout)
173
- this.ro.observe(document.documentElement)
172
+ if (sidebar) this.resizeObserver.observe(this.appLayoutSidebar())
173
+ if (pageLayout) this.resizeObserver.observe(pageLayout)
174
+ this.resizeObserver.observe(document.documentElement)
174
175
 
175
176
  // Recompute on page layout toggles / VT renders
176
177
  addEventListener("turbo:render", this.calculateTransformations)
@@ -180,7 +181,11 @@ export default class extends Controller {
180
181
  }
181
182
 
182
183
  teardownTransformations() {
183
- this.ro?.disconnect()
184
+ if (this.transformationsFrame) {
185
+ cancelAnimationFrame(this.transformationsFrame)
186
+ this.transformationsFrame = null
187
+ }
188
+ this.resizeObserver?.disconnect()
184
189
  removeEventListener("turbo:render", this.calculateTransformations)
185
190
  removeEventListener("turbo:before-render", this.calculateTransformations)
186
191
  }
@@ -230,6 +235,15 @@ export default class extends Controller {
230
235
  this.dispatch('scale-ratio-updated', { detail: { value: scaleRatio } })
231
236
  }
232
237
 
238
+ scheduleTransformations() {
239
+ if (this.transformationsFrame) return
240
+
241
+ this.transformationsFrame = requestAnimationFrame(() => {
242
+ this.transformationsFrame = null
243
+ this.calculateTransformations()
244
+ })
245
+ }
246
+
233
247
  calculateTransformX() {
234
248
  if (!this.hasExpandedPageLayout()) return 0
235
249
  return this.expandedPageLayout().offsetWidth - this.appLayoutSidebarWidth()
@@ -79,10 +79,16 @@ export default class extends Controller {
79
79
  // === SECTIONS ===
80
80
 
81
81
  addSection(event) {
82
- log('addSection', event.detail.fetchResponse.response.headers.get('X-Section-Id'), event.detail.fetchResponse.response.headers.get('X-Section-Position'))
83
- const sectionId = event.detail.fetchResponse.response.headers.get('X-Section-Id')
84
- const position = event.detail.fetchResponse.response.headers.get('X-Section-Position')
85
- this.postMessage('section:add', { sectionId, insertAt: parseInt(position) })
82
+ if (!event.detail.success) return
83
+
84
+ const res = event.detail.fetchResponse.response
85
+ log('addSection', res.headers.get('X-Section-Id'), res.headers.get('X-Section-Position'))
86
+
87
+ const sectionId = res.headers.get('X-Section-Id')
88
+ const position = res.headers.get('X-Section-Position')
89
+ const insertAt = position === null || position === '' ? undefined : parseInt(position, 10)
90
+
91
+ this.postMessage('section:add', { sectionId, insertAt })
86
92
  }
87
93
 
88
94
  deleteSection(event) {
@@ -3,9 +3,9 @@
3
3
  <% else %>
4
4
  <div class="flex flex-row items-center h-full w-full px-4 overflow-hidden">
5
5
  <div class="flex flex-col leading-none overflow-hidden">
6
- <div class="flex items-center">
7
- <%= helpers.maglev_page_icon(page, size: '1.1rem', wrapper_class_names: 'text-gray-900') %>
8
- <div class="text-base font-semibold truncate ml-1 mr-3">
6
+ <div class="flex items-center overflow-hidden">
7
+ <%= helpers.maglev_page_icon(page, size: '1.1rem', wrapper_class_names: 'text-gray-900 shrink-0') %>
8
+ <div class="text-base font-semibold truncate ml-1 mr-3 min-w-0">
9
9
  <%= page.title.presence || page.default_title %>
10
10
  </div>
11
11
 
@@ -16,8 +16,8 @@
16
16
  %>
17
17
  </div>
18
18
 
19
- <div class="text-xs text-gray-500 flex items-center space-x-1">
20
- <p class="truncate text-gray-700">
19
+ <div class="text-xs text-gray-500 flex items-center space-x-1 overflow-hidden">
20
+ <p class="truncate text-gray-700 min-w-0">
21
21
  <span class="text-gray-900 font-semibold">/</span><%= page_path %>
22
22
  </p>
23
23
  </div>
@@ -26,7 +26,7 @@ module Maglev
26
26
 
27
27
  def handle_stale_object
28
28
  respond_to do |format|
29
- format.turbo_stream { render 'maglev/editor/shared/errors/stale_object_error' }
29
+ format.turbo_stream { render 'maglev/editor/shared/errors/stale_object_error', status: :conflict }
30
30
  format.html { redirect_to editor_root_path }
31
31
  end
32
32
  end
@@ -39,6 +39,7 @@ module Maglev
39
39
  def track_maglev_error(error)
40
40
  Rails.logger.error "[Maglev] Error: #{error.message}"
41
41
  Rails.logger.error error.backtrace.join("\n")
42
+ Rails.error.report(error, handled: true)
42
43
  end
43
44
  end
44
45
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Editor
5
+ module LockVersionConcern
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ helper_method :source_lock_version
10
+ end
11
+
12
+ private
13
+
14
+ def lock_source
15
+ @section.site_scoped? ? maglev_site : current_maglev_page
16
+ end
17
+
18
+ def source_lock_version
19
+ lock_source.lock_version || 0
20
+ end
21
+
22
+ def reload_lock_source
23
+ lock_source.reload if @section.site_scoped?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -3,8 +3,9 @@
3
3
  module Maglev
4
4
  module Editor
5
5
  class SectionBlocksController < Maglev::Editor::BaseController
6
+ include Maglev::Editor::LockVersionConcern
7
+
6
8
  helper Maglev::Editor::SettingsHelper
7
- helper_method :source_lock_version
8
9
 
9
10
  before_action :set_section
10
11
  before_action :set_section_block, only: %i[edit update destroy]
@@ -32,6 +33,7 @@ module Maglev
32
33
 
33
34
  def update
34
35
  update_section_block
36
+ reload_lock_source
35
37
  flash.now[:notice] = flash_t(:success)
36
38
  end
37
39
 
@@ -77,14 +79,6 @@ module Maglev
77
79
  )
78
80
  end
79
81
 
80
- def lock_source
81
- @section.site_scoped? ? maglev_site : current_maglev_page
82
- end
83
-
84
- def source_lock_version
85
- lock_source.lock_version || 0
86
- end
87
-
88
82
  def redirect_to_section_blocks_path(success: true)
89
83
  flash = case success
90
84
  when true then { notice: flash_t(:success) }
@@ -3,8 +3,9 @@
3
3
  module Maglev
4
4
  module Editor
5
5
  class SectionsController < Maglev::Editor::BaseController
6
+ include Maglev::Editor::LockVersionConcern
7
+
6
8
  helper Maglev::Editor::SettingsHelper
7
- helper_method :source_lock_version
8
9
 
9
10
  before_action :ensure_turbo_frame_request, only: [:new]
10
11
  before_action :set_section, only: %i[edit update]
@@ -39,16 +40,17 @@ module Maglev
39
40
 
40
41
  def update
41
42
  update_section
43
+ reload_lock_source
42
44
  flash.now[:notice] = flash_t(:success)
43
45
  end
44
46
 
45
47
  def sort
46
- current_maglev_page.reorder_sections(params[:item_ids], params[:lock_version])
47
- if current_maglev_page.save
48
- redirect_to_sections_path
49
- else
50
- render_index_with_error
51
- end
48
+ services.sort_sections.call(
49
+ page: current_maglev_page,
50
+ section_ids: params[:item_ids],
51
+ lock_version: params[:lock_version]
52
+ )
53
+ redirect_to_sections_path
52
54
  end
53
55
 
54
56
  def destroy
@@ -95,14 +97,6 @@ module Maglev
95
97
  headers['X-Section-Position'] = flash[:position]
96
98
  end
97
99
 
98
- def lock_source
99
- @section.site_scoped? ? maglev_site : current_maglev_page
100
- end
101
-
102
- def source_lock_version
103
- lock_source.lock_version || 0
104
- end
105
-
106
100
  def redirect_to_sections_path
107
101
  redirect_to editor_sections_path_with_context, notice: flash_t(:success), status: :see_other
108
102
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Studio
5
+ # Renders a single section from default theme content on an in-memory page (no DB page load).
6
+ # :slug is the theme section type (e.g. "showcase"). Disabled outside +Rails.env.local?+.
7
+ class SectionPreviewController < ApplicationController
8
+ include Maglev::RenderingConcern
9
+ include Maglev::ServicesConcern
10
+ include Maglev::ContentLocaleConcern
11
+
12
+ rescue_from ActiveRecord::RecordNotFound, with: :respond_not_found
13
+ rescue_from Maglev::Errors::UnknownSection, with: :respond_not_found
14
+
15
+ before_action :fetch_maglev_site
16
+ around_action :with_default_site_locale
17
+
18
+ def show
19
+ render_maglev_page
20
+ end
21
+
22
+ private
23
+
24
+ def fetch_maglev_site
25
+ super.tap do |site|
26
+ raise ActiveRecord::RecordNotFound if site.nil?
27
+
28
+ maglev_services.context.site = site
29
+ end
30
+ end
31
+
32
+ def fetch_maglev_page
33
+ @fetch_maglev_page ||= build_preview_page
34
+ end
35
+
36
+ def build_preview_page
37
+ theme = fetch_maglev_theme
38
+ section_content = build_section_content_for_preview(theme)
39
+
40
+ Maglev::Page.new(title: 'Section preview', path: 'index').tap do |page|
41
+ page.sections = [section_content]
42
+ page.prepare_sections(theme)
43
+ end
44
+ end
45
+
46
+ def build_section_content_for_preview(theme)
47
+ definition = theme.sections.find(params[:slug].to_s)
48
+ raise Maglev::Errors::UnknownSection unless definition
49
+
50
+ definition.build_default_content.with_indifferent_access
51
+ end
52
+
53
+ def fetch_maglev_page_sections(*)
54
+ section_id = fetch_maglev_page.sections.first.fetch('id')
55
+
56
+ @fetch_maglev_page_sections ||= maglev_services.get_page_sections.call(
57
+ page: fetch_maglev_page,
58
+ section_id: section_id,
59
+ locale: content_locale
60
+ )
61
+ end
62
+
63
+ def maglev_rendering_mode
64
+ :section
65
+ end
66
+
67
+ def with_default_site_locale(&block)
68
+ Maglev::I18n.with_locale(maglev_site.default_locale_prefix, &block)
69
+ end
70
+
71
+ def respond_not_found
72
+ head :not_found
73
+ end
74
+ end
75
+ end
76
+ end
@@ -19,6 +19,31 @@ module Maglev
19
19
  maglev_importmap_tags(:client, 'maglev-client')
20
20
  end
21
21
 
22
+ # Use this helper in your theme layout instead of calling javascript_importmap_tags
23
+ # and maglev_client_javascript_tags separately.
24
+ #
25
+ # Firefox (unlike Chrome) strictly follows the HTML spec which only allows a single
26
+ # <script type="importmap"> per document and ignores any subsequent ones. When both
27
+ # the theme and the Maglev client each emit their own importmap, Firefox silently
28
+ # discards the second one, causing `import "maglev-client"` to fail with:
29
+ # "The specifier 'maglev-client' was a bare specifier, but was not remapped to anything."
30
+ #
31
+ # This helper solves the problem by merging the Maglev client importmap entries into
32
+ # the theme's importmap so that only one <script type="importmap"> is ever emitted.
33
+ # Once browsers universally support multiple importmaps (Chrome already does),
34
+ # this helper can be simplified back to two separate calls.
35
+ def maglev_javascript_importmap_tags(entry_point = 'application', importmap: Rails.application.importmap)
36
+ return javascript_importmap_tags(entry_point, importmap: importmap) unless maglev_rendering_mode == :editor
37
+
38
+ safe_join [
39
+ javascript_inline_importmap_tag(maglev_merged_importmap_json(importmap)),
40
+ javascript_importmap_module_preload_tags(importmap, entry_point: entry_point),
41
+ javascript_importmap_module_preload_tags(Maglev::Engine.importmaps[:client], entry_point: 'maglev-client'),
42
+ javascript_import_module_tag(entry_point),
43
+ javascript_import_module_tag('maglev-client')
44
+ ], "\n"
45
+ end
46
+
22
47
  def maglev_importmap_tags(namespace, entry_point)
23
48
  safe_join [
24
49
  javascript_inline_importmap_tag(Maglev::Engine.importmaps[namespace].to_json(resolver: self)),
@@ -176,6 +201,15 @@ module Maglev
176
201
  # rubocop:enable Layout/LineLength
177
202
  maglev_client_javascript_tags
178
203
  end
204
+
205
+ private
206
+
207
+ def maglev_merged_importmap_json(theme_importmap)
208
+ theme_map = JSON.parse(theme_importmap.to_json(resolver: self))
209
+ client_map = JSON.parse(Maglev::Engine.importmaps[:client].to_json(resolver: self))
210
+ theme_map['imports'].merge!(client_map['imports'])
211
+ theme_map.to_json
212
+ end
179
213
  end
180
214
  end
181
215
  # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Studio
5
+ module SectionPreviewHelper
6
+ include Maglev::PagePreviewHelper
7
+ end
8
+ end
9
+ end
@@ -33,12 +33,6 @@ module Maglev::SectionsConcern
33
33
  sections&.find_all { |section| section['type'] == type } || []
34
34
  end
35
35
 
36
- def reorder_sections(sorted_section_ids, lock_version)
37
- self.lock_version = lock_version
38
- sections_translations_will_change!
39
- sections.sort! { |a, b| sorted_section_ids.index(a['id']) <=> sorted_section_ids.index(b['id']) }
40
- end
41
-
42
36
  def delete_section(section_id)
43
37
  sections_translations_will_change!
44
38
  sections.delete_if { |section| section['id'] == section_id }
@@ -58,7 +58,7 @@ module Maglev
58
58
 
59
59
  def update_setting_value(setting, current_content)
60
60
  setting_content = current_content.find { |s| s['id'] == setting.id }
61
- value = content[setting.id.to_sym]
61
+ value = setting.cast_value(content[setting.id.to_sym])
62
62
 
63
63
  if setting_content.nil?
64
64
  current_content.push({ id: setting.id, value: value })
@@ -57,6 +57,8 @@ module Maglev
57
57
  depends_on: %i[fetch_site fetch_theme]
58
58
  dependency :delete_section, class: Maglev::Content::DeleteSectionService,
59
59
  depends_on: %i[fetch_site fetch_theme]
60
+ dependency :sort_sections, class: Maglev::Content::SortSectionsService,
61
+ depends_on: %i[fetch_site fetch_theme]
60
62
 
61
63
  dependency :add_section_block, class: Maglev::Content::AddSectionBlockService,
62
64
  depends_on: %i[fetch_site fetch_theme]
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module Content
5
+ class SortSectionsService
6
+ include Injectable
7
+
8
+ dependency :fetch_theme
9
+ dependency :fetch_site
10
+
11
+ argument :page
12
+ argument :section_ids
13
+ argument :lock_version, default: nil
14
+
15
+ def call
16
+ check_lock_version!
17
+ sort_sections!
18
+ end
19
+
20
+ private
21
+
22
+ def sort_sections!
23
+ sorted_ids = normalized_section_ids
24
+ page.sections_translations_will_change!
25
+ page.sections.sort! { |a, b| sorted_ids.index(a['id']) <=> sorted_ids.index(b['id']) }
26
+ page.save!
27
+ end
28
+
29
+ def check_lock_version!
30
+ return if lock_version.blank?
31
+
32
+ current_version = page.lock_version.to_i
33
+ page.lock_version = lock_version.to_i
34
+
35
+ return if current_version == lock_version.to_i
36
+
37
+ raise ActiveRecord::StaleObjectError.new(page, 'sort_sections')
38
+ end
39
+
40
+ def normalized_section_ids
41
+ # For site-scoped sections, transform_if_site_scoped replaces the page-local ID
42
+ # with the site section's ID at render time, so the client sends site IDs.
43
+ # Legacy pages (created before IDs were unified) store a different page-local ID.
44
+ # Build a mapping: display_id (what the client sends) → page_id (what page.sections stores).
45
+ id_map = page.sections.each_with_object({}) do |page_section, map|
46
+ display_id = site_display_id_for(page_section) || page_section['id']
47
+ map[display_id] = page_section['id']
48
+ end
49
+ section_ids.map { |id| id_map[id] || id }
50
+ end
51
+
52
+ def site_display_id_for(page_section)
53
+ return unless theme.sections.find(page_section['type'])&.site_scoped?
54
+
55
+ site.find_section(page_section['type'])&.dig('id')
56
+ end
57
+
58
+ def theme
59
+ @theme ||= fetch_theme.call
60
+ end
61
+
62
+ def site
63
+ @site ||= fetch_site.call
64
+ end
65
+ end
66
+ end
67
+ end
@@ -10,8 +10,18 @@ module Maglev
10
10
  argument :absolute, default: false
11
11
 
12
12
  def call
13
- path = "#{fetch_sections_path.call(theme: theme)}/#{section.category}/#{section.id}.jpg"
13
+ path = "#{base_path}.#{extension}"
14
14
  absolute ? Rails.root.join("public/#{path}").to_s : "/#{path}"
15
15
  end
16
+
17
+ private
18
+
19
+ def base_path
20
+ "#{fetch_sections_path.call(theme: theme)}/#{section.category}/#{section.id}"
21
+ end
22
+
23
+ def extension
24
+ File.exist?(Rails.root.join("public/#{base_path}.webp")) ? 'webp' : 'jpg'
25
+ end
16
26
  end
17
27
  end
@@ -30,5 +30,4 @@
30
30
  <div class="flex items-center pt-3">
31
31
  <%= render Maglev::Uikit::PaginationComponent.new(pagy: @pagy, hidden_if_single_page: false) %>
32
32
  </div>
33
- </div>
34
33
  <% end %>
@@ -13,7 +13,10 @@
13
13
  options: {
14
14
  placeholder: t('maglev.editor.image_library.search_placeholder'),
15
15
  data: { turbo_frame: 'asset-search-results' }
16
- }) %>
16
+ }) do %>
17
+ <%= hidden_field_tag :picker, params[:picker] if params[:picker].present? %>
18
+ <%= hidden_field_tag :source, params[:source] if params[:source].present? %>
19
+ <% end %>
17
20
 
18
21
  </div>
19
22
  <div class="ml-auto">
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Studio-only utilities (controllers reject requests outside +Rails.env.local?+).
4
+ namespace :studio, path: 'studio' do
5
+ get 'section_preview/:slug', to: 'section_preview#show', as: :section_preview
6
+ end
data/config/routes.rb CHANGED
@@ -9,6 +9,9 @@ Maglev::Engine.routes.draw do
9
9
  # Preview
10
10
  draw 'maglev/preview'
11
11
 
12
+ # Studio (for local usage)
13
+ draw 'maglev/studio' if Rails.env.local?
14
+
12
15
  # Public Assets
13
16
  draw 'maglev/assets'
14
17
 
@@ -11,7 +11,7 @@
11
11
 
12
12
  <%%= render_maglev_alternate_links %>
13
13
 
14
- <%%= maglev_client_javascript_tags %>
14
+ <%%= maglev_javascript_importmap_tags %>
15
15
 
16
16
  <%%=
17
17
  javascript_include_tag(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Maglev
4
- VERSION = '3.0.1'
4
+ VERSION = '3.0.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maglevcms
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Didier Lafforgue
@@ -359,6 +359,7 @@ files:
359
359
  - app/controllers/concerns/maglev/back_action_concern.rb
360
360
  - app/controllers/concerns/maglev/content_locale_concern.rb
361
361
  - app/controllers/concerns/maglev/editor/errors_concern.rb
362
+ - app/controllers/concerns/maglev/editor/lock_version_concern.rb
362
363
  - app/controllers/concerns/maglev/editor/preview_urls_concern.rb
363
364
  - app/controllers/concerns/maglev/editor/turbo_concern.rb
364
365
  - app/controllers/concerns/maglev/errors_concern.rb
@@ -391,6 +392,7 @@ files:
391
392
  - app/controllers/maglev/published_page_preview_controller.rb
392
393
  - app/controllers/maglev/site_controller.rb
393
394
  - app/controllers/maglev/sitemap_controller.rb
395
+ - app/controllers/maglev/studio/section_preview_controller.rb
394
396
  - app/helpers/maglev/admin/sections/previews_helper.rb
395
397
  - app/helpers/maglev/admin/themes_helper.rb
396
398
  - app/helpers/maglev/application_helper.rb
@@ -399,6 +401,7 @@ files:
399
401
  - app/helpers/maglev/editor/settings_helper.rb
400
402
  - app/helpers/maglev/page_preview_helper.rb
401
403
  - app/helpers/maglev/sitemap_helper.rb
404
+ - app/helpers/maglev/studio/section_preview_helper.rb
402
405
  - app/jobs/maglev/application_job.rb
403
406
  - app/lib/maglev/form/form_builder.rb
404
407
  - app/lib/maglev/form/inputs/check_box.rb
@@ -462,6 +465,7 @@ files:
462
465
  - app/services/maglev/content/delete_section_block_service.rb
463
466
  - app/services/maglev/content/delete_section_service.rb
464
467
  - app/services/maglev/content/sort_section_blocks_service.rb
468
+ - app/services/maglev/content/sort_sections_service.rb
465
469
  - app/services/maglev/content/update_section_block_service.rb
466
470
  - app/services/maglev/content/update_section_service.rb
467
471
  - app/services/maglev/create_page_service.rb
@@ -579,6 +583,7 @@ files:
579
583
  - config/routes/maglev/editor.rb
580
584
  - config/routes/maglev/preview.rb
581
585
  - config/routes/maglev/public_preview.rb
586
+ - config/routes/maglev/studio.rb
582
587
  - db/migrate/20200824085207_create_maglev_sites.rb
583
588
  - db/migrate/20200824104648_create_maglev_pages.rb
584
589
  - db/migrate/20200831101942_create_maglev_section_content.rb