maglevcms 3.0.1 → 3.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3061afa4c3b8ce916e981ea025e8db6b63102287b152163369813ec9ac172b5e
4
- data.tar.gz: 484f90090316bd4a319e4223b5d86d5c673260f56808386f2f792a1b12613331
3
+ metadata.gz: 862336e896c5cea49b17ff58d5837b8d3e301580f8dbe5966d35e9ab378cbf04
4
+ data.tar.gz: 2d49e3d1f3ddf480906efbffc2477073a65f66694593bfe347b9faf75ec3c798
5
5
  SHA512:
6
- metadata.gz: 2c2df97bdeee5ff72c3c2fb7796e3d14b23169a71c68bb91879a002aa944e678f8fe1461ada1b3512394bf5f4ba848c35636345023026549fe15c964527d28ea
7
- data.tar.gz: 56665addc22ceb997563182a3217ebfc6fcfdefa9a619ea34ed6e1dcdbe3c7a3fc5aef6f14c041df2e081cbc78f9abb11bd7f3121ee21681c62f5b16b8309b5e
6
+ metadata.gz: 232bde061cabd3d356e6623cfdddfc4c26c2f82c3a4ace19eafe52667afe1cb94afbf7705d03519ec77a65145ab5a3fddbc41ee9e0ff10960973fe8282fe2010
7
+ data.tar.gz: 3fc4998c42adb572d2f930eafb255872e90560d4330517181fd6457da0f7d8ea4f09e84ea43669ffba5dcd2b8187b43e0f263d9fa93801e07de79dcaec207d3e
@@ -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) => {
@@ -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>
@@ -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
@@ -43,12 +43,12 @@ module Maglev
43
43
  end
44
44
 
45
45
  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
46
+ services.sort_sections.call(
47
+ page: current_maglev_page,
48
+ section_ids: params[:item_ids],
49
+ lock_version: params[:lock_version]
50
+ )
51
+ redirect_to_sections_path
52
52
  end
53
53
 
54
54
  def destroy
@@ -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 }
@@ -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.2'
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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Didier Lafforgue
@@ -391,6 +391,7 @@ files:
391
391
  - app/controllers/maglev/published_page_preview_controller.rb
392
392
  - app/controllers/maglev/site_controller.rb
393
393
  - app/controllers/maglev/sitemap_controller.rb
394
+ - app/controllers/maglev/studio/section_preview_controller.rb
394
395
  - app/helpers/maglev/admin/sections/previews_helper.rb
395
396
  - app/helpers/maglev/admin/themes_helper.rb
396
397
  - app/helpers/maglev/application_helper.rb
@@ -399,6 +400,7 @@ files:
399
400
  - app/helpers/maglev/editor/settings_helper.rb
400
401
  - app/helpers/maglev/page_preview_helper.rb
401
402
  - app/helpers/maglev/sitemap_helper.rb
403
+ - app/helpers/maglev/studio/section_preview_helper.rb
402
404
  - app/jobs/maglev/application_job.rb
403
405
  - app/lib/maglev/form/form_builder.rb
404
406
  - app/lib/maglev/form/inputs/check_box.rb
@@ -462,6 +464,7 @@ files:
462
464
  - app/services/maglev/content/delete_section_block_service.rb
463
465
  - app/services/maglev/content/delete_section_service.rb
464
466
  - app/services/maglev/content/sort_section_blocks_service.rb
467
+ - app/services/maglev/content/sort_sections_service.rb
465
468
  - app/services/maglev/content/update_section_block_service.rb
466
469
  - app/services/maglev/content/update_section_service.rb
467
470
  - app/services/maglev/create_page_service.rb
@@ -579,6 +582,7 @@ files:
579
582
  - config/routes/maglev/editor.rb
580
583
  - config/routes/maglev/preview.rb
581
584
  - config/routes/maglev/public_preview.rb
585
+ - config/routes/maglev/studio.rb
582
586
  - db/migrate/20200824085207_create_maglev_sites.rb
583
587
  - db/migrate/20200824104648_create_maglev_pages.rb
584
588
  - db/migrate/20200831101942_create_maglev_section_content.rb