maglevcms 3.0.0.beta2 → 3.0.0.beta3

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/maglev/tailwind.css +523 -11852
  3. data/app/assets/javascripts/maglev/editor/controllers/app/preview_notification_center_controller.js +1 -1
  4. data/app/assets/javascripts/maglev/editor/controllers/shared/submit_button_controller.js +29 -5
  5. data/app/assets/javascripts/maglev/editor/controllers/utils.js +16 -0
  6. data/app/assets/javascripts/maglev/editor/index.js +3 -43
  7. data/app/assets/javascripts/maglev/editor/patches/page_renderer_patch.js +47 -0
  8. data/app/assets/javascripts/maglev/editor/patches/turbo_delayed_streams.js +35 -0
  9. data/app/assets/javascripts/maglev/editor/patches/turbo_stream_patch.js +50 -0
  10. data/app/assets/stylesheets/maglev/application.css +0 -2
  11. data/app/assets/stylesheets/maglev/tailwind.css.erb +3 -1
  12. data/app/components/maglev/uikit/app_layout/topbar/logo_component.html.erb +1 -1
  13. data/app/components/maglev/uikit/app_layout/topbar/page_info_component.html.erb +1 -1
  14. data/app/components/maglev/uikit/dropdown_component/dropdown_controller.js +6 -1
  15. data/app/controllers/concerns/maglev/editor/errors_concern.rb +9 -9
  16. data/app/controllers/concerns/maglev/errors_concern.rb +17 -0
  17. data/app/controllers/maglev/application_controller.rb +1 -0
  18. data/app/controllers/maglev/site_controller.rb +15 -0
  19. data/app/helpers/maglev/application_helper.rb +20 -2
  20. data/app/models/maglev/page.rb +14 -0
  21. data/app/models/maglev/site.rb +5 -0
  22. data/app/services/maglev/fetch_site.rb +3 -1
  23. data/app/services/maglev/get_published_page_sections_service.rb +1 -1
  24. data/app/services/maglev/publish_service.rb +3 -0
  25. data/app/views/layouts/maglev/editor/_sidebar.html.erb +2 -1
  26. data/app/views/layouts/maglev/editor/_topbar.html.erb +6 -23
  27. data/app/views/layouts/maglev/editor/application.html.erb +1 -0
  28. data/app/views/layouts/maglev/editor/topbar/_page_info.html.erb +11 -0
  29. data/app/views/layouts/maglev/editor/topbar/_publish_button.html.erb +13 -0
  30. data/app/views/maglev/editor/publication/create.turbo_stream.erb +3 -1
  31. data/app/views/maglev/editor/section_blocks/update.turbo_stream.erb +9 -1
  32. data/app/views/maglev/editor/sections/update.turbo_stream.erb +9 -1
  33. data/app/views/maglev/errors/site_not_found.html.erb +33 -0
  34. data/config/editor_importmap.rb +3 -0
  35. data/config/routes.rb +3 -0
  36. data/db/migrate/20251116171603_add_published_at_to_sites_and_pages.rb +6 -0
  37. data/lib/maglev/engine.rb +1 -0
  38. data/lib/maglev/errors.rb +1 -0
  39. data/lib/maglev/version.rb +1 -1
  40. data/lib/tasks/maglev/tailwindcss.rake +1 -0
  41. metadata +10 -1
@@ -1,5 +1,5 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
- import { isSamePath } from"maglev-controllers/utils"
2
+ import { isSamePath } from "maglev-controllers/utils"
3
3
 
4
4
  export default class extends Controller {
5
5
  static targets = ["iframe"]
@@ -1,25 +1,41 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { sleep } from"maglev-controllers/utils"
3
+ import TurboDelayedStreams from 'maglev-patches/turbo_delayed_streams'
3
4
 
4
5
  export default class extends Controller {
5
6
 
6
7
  initialize() {
7
8
  this._start = this.start.bind(this)
8
9
  this._end = this.end.bind(this)
10
+ this._cancel = this.cancel.bind(this)
9
11
  }
10
12
 
11
13
  connect() {
14
+ this.canceled = false
12
15
  this.formElement = this.element.closest('form')
13
16
  this.formElement.addEventListener('turbo:submit-start', this._start)
14
17
  this.formElement.addEventListener('turbo:submit-end', this._end)
18
+ document.addEventListener("turbo:before-cache", this._cancel)
15
19
  }
16
20
 
17
21
  disconnect() {
22
+ this.canceled = true
23
+ this.requestId = null
18
24
  this.formElement.removeEventListener('turbo:submit-start', this._start)
19
- this.formElement.removeEventListener('turbo:submit-end', this._end)
25
+ this.formElement.removeEventListener('turbo:submit-end', this._end)
20
26
  }
21
27
 
22
- start() {
28
+ cancel() {
29
+ this.canceled = true
30
+ this.element.disabled = false
31
+ this.formElement.classList.remove('is-pending', 'is-success', 'is-error')
32
+ this.formElement.classList.add('is-default')
33
+ }
34
+
35
+ start(event) {
36
+ // the requestId is set by the turbo:before-fetch-request listener. We need it if there are delayed streams to render
37
+ this.requestId = event.detail.formSubmission.fetchRequest.fetchOptions.turboRequestId
38
+
23
39
  this.formElement.classList.add('is-pending')
24
40
  this.startedAt = Date.now()
25
41
  }
@@ -30,16 +46,24 @@ export default class extends Controller {
30
46
 
31
47
  // on an UX standpoint, we want to show the pending state for a short time to avoid flickering
32
48
  // if the submit (call to the server) took less than 800ms, we wait for 800ms to show the pending state
33
- if (Date.now() - this.startedAt < 800) await sleep(800)
49
+ if (Date.now() - this.startedAt < 600) await sleep(600)
50
+
51
+ if (this.canceled) return
34
52
 
35
53
  this.formElement.classList.remove('is-pending')
36
- this.formElement.classList.add(event.detail.success ? 'is-success' : 'is-error')
54
+ this.formElement.classList.add(event.detail.success ? 'is-success' : 'is-error')
37
55
 
38
56
  // wait for 2 seconds and then remove the success or error class and add the default class
39
- await sleep(1600)
57
+ await sleep(1400)
58
+
59
+ if (this.canceled) return
40
60
 
41
61
  this.formElement.classList.remove(event.detail.success ? 'is-success' : 'is-error')
42
62
  this.formElement.classList.add('is-default')
43
63
  this.element.disabled = false
64
+
65
+ // render the delayed turbo stream messages from the request
66
+ // use the requestId to identify the request and render the turbo stream messages
67
+ TurboDelayedStreams.render(this.requestId)
44
68
  }
45
69
  }
@@ -7,4 +7,20 @@ export const isSamePath = (targetPath) => {
7
7
 
8
8
  export const sleep = (ms) => {
9
9
  return new Promise(resolve => setTimeout(resolve, ms))
10
+ }
11
+
12
+ function uuidv4() {
13
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
14
+ const r = Math.random() * 16 | 0
15
+ const v = c === "x" ? r : (r & 0x3 | 0x8)
16
+ return v.toString(16)
17
+ })
18
+ }
19
+
20
+ export function generateRequestId() {
21
+ // use the native randomUUID function if available (Browsers don't expose it for non-secure domains)
22
+ if (window.crypto?.randomUUID) {
23
+ return window.crypto.randomUUID()
24
+ }
25
+ return uuidv4()
10
26
  }
@@ -1,6 +1,7 @@
1
1
  import "@hotwired/turbo-rails"
2
2
  import "maglev-controllers"
3
- import { PageRenderer, StreamActions } from '@hotwired/turbo'
3
+ import "maglev-patches/page_renderer_patch"
4
+ import "maglev-patches/turbo_stream_patch"
4
5
 
5
6
  console.log('Maglev Editor v2 ⚡️')
6
7
 
@@ -22,45 +23,4 @@ document.addEventListener("click", (event) => {
22
23
  if (current.pathname === target.pathname && current.search === target.search) {
23
24
  link.dataset.turboAction = "replace"
24
25
  }
25
- })
26
-
27
- // iFrames will always be reloaded even if we mark them as turbo-permanent.
28
- // In the context of the Editor, the workaround is to replace the content of another DIV (#root)
29
- // instead of the body and put the preview iframe as a sibling of the root DIV.
30
- PageRenderer.prototype.assignNewBody = function() {
31
- const body = document.querySelector("#root")
32
- const el = this.newElement.querySelector("#root")
33
-
34
- // replace all the data attributes of the BODY tag with the data attributes of the newElement
35
- // This is because the BODY tag carries the current page id in one of the controller values
36
- const bodyDataAttributes = document.body.dataset
37
- const newElementDataAttributes = this.newElement.dataset
38
- Object.keys(newElementDataAttributes).forEach(key => {
39
- bodyDataAttributes[key] = newElementDataAttributes[key]
40
- })
41
-
42
- // now replace the "body" with the newElement
43
- if (body && el) {
44
- body.replaceWith(el)
45
- return
46
- } else if (document.body && this.newElement instanceof HTMLBodyElement) {
47
- document.body.replaceWith(this.newElement)
48
- } else {
49
- document.documentElement.appendChild(this.newElement)
50
- }
51
- }
52
-
53
- // Custom stream actions
54
-
55
- StreamActions.console_log = function() {
56
- const message = this.getAttribute("message")
57
- console.log(message)
58
- }
59
-
60
- StreamActions.dispatch_event = function() {
61
- const type = this.getAttribute("type")
62
- const payload = this.getAttribute("payload")
63
- console.log('dispatchEvent', type, payload, `dispatcher:${type}`)
64
- const event = new CustomEvent(`dispatcher:${type}`, { detail: JSON.parse(payload) })
65
- window.dispatchEvent(event)
66
- }
26
+ })
@@ -0,0 +1,47 @@
1
+ import { PageRenderer } from '@hotwired/turbo'
2
+
3
+ // iFrames will always be reloaded even if we mark them as turbo-permanent.
4
+ // In the context of the Editor, the workaround is to replace the content of another DIV (#root)
5
+ // instead of the body and put the preview iframe as a sibling of the root DIV.
6
+ // assignNewBody is the right function to patch because it's called by both the PageRenderer and the SnapshotRenderer.
7
+
8
+ // Inspirations:
9
+ // - https://github.com/hotwired/turbo/pull/711/files
10
+ // - https://github.com/Challenge-Guy/turbo-cfm1/commit/2a3b0acfe0367f32c9d1635ff7c6fd7d87d2a2cd
11
+ PageRenderer.prototype.assignNewBody = function() {
12
+ const body = document.querySelector("#root")
13
+ const el = this.newElement.querySelector("#root")
14
+
15
+ // Only update sync elements during snapshot preview render
16
+ // those elements are permanent during the snapshot preview render and updated when we've got the new snapshot
17
+ if (this.isPreview) {
18
+ const syncElements = document.querySelectorAll('[data-turbo-sync]')
19
+ syncElements.forEach(element => {
20
+ const currentElement = document.querySelector(`#${element.id}`)
21
+ const newElement = this.newElement.querySelector(`#${element.id}`)
22
+
23
+ if (currentElement && newElement) {
24
+ // sync the innerHTML of the newElement with the currentElement
25
+ newElement.innerHTML = currentElement.innerHTML
26
+ }
27
+ })
28
+ }
29
+
30
+ // replace all the data attributes of the BODY tag with the data attributes of the newElement
31
+ // This is because the BODY tag carries the current page id in one of the controller values
32
+ const bodyDataAttributes = document.body.dataset
33
+ const newElementDataAttributes = this.newElement.dataset
34
+ Object.keys(newElementDataAttributes).forEach(key => {
35
+ bodyDataAttributes[key] = newElementDataAttributes[key]
36
+ })
37
+
38
+ // now replace the "body" with the newElement
39
+ if (body && el) {
40
+ body.replaceWith(el)
41
+ return
42
+ } else if (document.body && this.newElement instanceof HTMLBodyElement) {
43
+ document.body.replaceWith(this.newElement)
44
+ } else {
45
+ document.documentElement.appendChild(this.newElement)
46
+ }
47
+ }
@@ -0,0 +1,35 @@
1
+ // Delayed Turbo streams are streams that are not rendered immediately, but are rendered later manually by the application
2
+ // It's used by the SubmitButton controller for instance
3
+
4
+ class TurboDelayedStream {
5
+ constructor(maxElements = 10) {
6
+ this.maxElements = maxElements
7
+ this.elements = new Map()
8
+ }
9
+
10
+ add(requestId, render) {
11
+ if (this.elements.has(requestId)) {
12
+ this.elements.get(requestId).push(render)
13
+ } else {
14
+ this.elements.set(requestId, [render])
15
+ }
16
+
17
+ if (this.elements.size > this.maxElements) {
18
+ this.elements.delete(this.elements.keys().next().value)
19
+ }
20
+ }
21
+
22
+ render(requestId) {
23
+ if (!this.elements.has(requestId)) return
24
+
25
+ const renderFns = this.elements.get(requestId)
26
+
27
+ // render all the turbo streams for this request
28
+ renderFns.forEach(render => render())
29
+
30
+ // remove the element from the list
31
+ this.elements.delete(requestId)
32
+ }
33
+ }
34
+
35
+ export default new TurboDelayedStream()
@@ -0,0 +1,50 @@
1
+
2
+ import { StreamActions } from '@hotwired/turbo'
3
+ import TurboDelayedStreams from 'maglev-patches/turbo_delayed_streams'
4
+ import { generateRequestId } from 'maglev-controllers/utils'
5
+
6
+ // Custom stream actions
7
+ StreamActions.console_log = function() {
8
+ const message = this.getAttribute("message")
9
+ console.log(message)
10
+ }
11
+
12
+ StreamActions.dispatch_event = function() {
13
+ const type = this.getAttribute("type")
14
+ const payload = this.getAttribute("payload")
15
+ console.log('dispatchEvent', type, payload, `dispatcher:${type}`)
16
+ const event = new CustomEvent(`dispatcher:${type}`, { detail: JSON.parse(payload) })
17
+ window.dispatchEvent(event)
18
+ }
19
+
20
+ // Handle delayed streams through Turbo events
21
+
22
+ document.addEventListener("turbo:before-stream-render", (event) => {
23
+ const delayedStream = (
24
+ event.detail.newStream.templateContent.querySelector('meta[name="turbo-delayed-stream"]')?.content ?? event.detail.newStream.getAttribute('delayed')
25
+ ) === 'true'
26
+ const requestId = (
27
+ event.detail.newStream.templateContent.querySelector('meta[name="turbo-request-id"]')?.content ?? event.detail.newStream.getAttribute('request-id')
28
+ )?.split(',')?.[0]
29
+
30
+ if (delayedStream) {
31
+ // Keep the stream in the queue to be rendered later
32
+ TurboDelayedStreams.add(requestId, () => {
33
+ // console.log('rendering delayed stream', requestId, event.detail.newStream)
34
+ event.detail.render(event.detail.newStream)
35
+ })
36
+
37
+ // Cancel Turbo's rendering for this stream
38
+ event.preventDefault()
39
+ }
40
+ })
41
+
42
+ document.addEventListener('turbo:before-fetch-request', (event) => {
43
+ const requestId = generateRequestId()
44
+
45
+ // Attach to headers (for server → client stream correlation)
46
+ event.detail.fetchOptions.headers["X-Turbo-Request-ID"] = requestId
47
+
48
+ // Attach to the fetchRequest instance
49
+ event.detail.fetchOptions.turboRequestId = requestId
50
+ })
@@ -10,8 +10,6 @@
10
10
  * files in this directory. Styles in this file should be added after the last require_* statement.
11
11
  * It is generally better to create a new file per style scope.
12
12
  *
13
- *= require_tree .
14
- *= require_self
15
13
  */
16
14
 
17
15
  @keyframes slide-out {
@@ -8,7 +8,9 @@
8
8
  --color-editor-primary: var(--editor-color-primary);
9
9
  }
10
10
 
11
- @source "../../../";
11
+ @source "<%= Maglev::Engine.root.join('app') %>";
12
+ @source "<%= Maglev::Engine.root.join('lib') %>";
13
+
12
14
  <% Maglev.config.tailwindcss_folders.each do |folder| %>
13
15
  @source "<%= folder %>";
14
16
  <% end %>
@@ -1,3 +1,3 @@
1
- <%= link_to root_path, class: "w-16 h-full flex justify-center items-center border-r border-gray-200 shrink-0" do %>
1
+ <%= link_to root_path, class: "w-16 h-full flex justify-center items-center border-r border-gray-200 shrink-0", data: { controller: 'prevent-same-path' } do %>
2
2
  <%= image_tag logo_url, class: "w-2/4", id: 'editor-logo-image', data: { turbo_permanent: true } %>
3
3
  <% end %>
@@ -4,7 +4,7 @@
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
6
  <div class="flex items-center">
7
- <%= render Maglev::Uikit::IconComponent.new(name: icon_name, size: '1.1rem', class_names: 'shrink-0 text-gray-900') %>
7
+ <%= helpers.maglev_page_icon(page, size: '1.1rem', wrapper_class_names: 'text-gray-900') %>
8
8
  <div class="text-base font-semibold truncate ml-1 mr-3">
9
9
  <%= page.title.presence || page.default_title %>
10
10
  </div>
@@ -47,7 +47,12 @@ export default class extends Controller {
47
47
  }
48
48
 
49
49
  disconnect() {
50
- this.cleanup()
50
+ this.cleanup() // clean up the observer for the floating element
51
+
52
+ // fix issue: https://github.com/stimulus-use/stimulus-use/issues/500
53
+ this.enter = null
54
+ this.leave = null
55
+ this.toggleTransition = null
51
56
  }
52
57
 
53
58
  clickOutside() {
@@ -7,23 +7,16 @@ module Maglev
7
7
 
8
8
  included do
9
9
  around_action :handle_editor_errors
10
-
11
- rescue_from ActiveRecord::StaleObjectError, with: :handle_stale_object
12
10
  end
13
11
 
14
12
  private
15
13
 
16
- def handle_stale_object
17
- respond_to do |format|
18
- format.turbo_stream { render 'maglev/editor/shared/errors/stale_object_error' }
19
- format.html { redirect_to editor_root_path }
20
- end
21
- end
22
-
23
14
  def handle_editor_errors
24
15
  yield
25
16
  rescue Maglev::Errors::NotAuthorized
26
17
  raise # Let the main app handle this one
18
+ rescue ActiveRecord::StaleObjectError
19
+ handle_stale_object
27
20
  rescue StandardError => e
28
21
  respond_to do |format|
29
22
  format.turbo_stream { render_turbo_stream_standard_error(e) }
@@ -31,6 +24,13 @@ module Maglev
31
24
  end
32
25
  end
33
26
 
27
+ def handle_stale_object
28
+ respond_to do |format|
29
+ format.turbo_stream { render 'maglev/editor/shared/errors/stale_object_error' }
30
+ format.html { redirect_to editor_root_path }
31
+ end
32
+ end
33
+
34
34
  def render_turbo_stream_standard_error(error)
35
35
  track_maglev_error(error)
36
36
  render 'maglev/editor/shared/errors/standard_error', status: :internal_server_error
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ module ErrorsConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ rescue_from ::Maglev::Errors::SiteNotFound, with: :handle_site_not_found if Rails.env.development?
9
+ end
10
+
11
+ private
12
+
13
+ def handle_site_not_found
14
+ render 'maglev/errors/site_not_found', layout: nil, status: :not_found
15
+ end
16
+ end
17
+ end
@@ -4,6 +4,7 @@ module Maglev
4
4
  class ApplicationController < ::ApplicationController
5
5
  include Maglev::ServicesConcern
6
6
  include Maglev::ResourceIdConcern
7
+ include Maglev::ErrorsConcern
7
8
 
8
9
  protect_from_forgery with: :exception
9
10
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ class SiteController < Maglev::ApplicationController
5
+ def create
6
+ redirect_to editor_root_path and return if Maglev::Site.exists? || !Rails.env.local?
7
+
8
+ Maglev::GenerateSite.call(
9
+ theme: Maglev.local_themes.first
10
+ )
11
+
12
+ redirect_to editor_root_path
13
+ end
14
+ end
15
+ end
@@ -3,6 +3,7 @@
3
3
  # rubocop:disable Metrics/ModuleLength
4
4
  module Maglev
5
5
  module ApplicationHelper
6
+ ## "system" helpers
6
7
  def turbo_stream
7
8
  # we don't want to pollute the global Turbo::Streams::TagBuilder
8
9
  Maglev::Turbo::Streams::TagBuilder.new(self)
@@ -26,6 +27,17 @@ module Maglev
26
27
  ], "\n"
27
28
  end
28
29
 
30
+ def maglev_delayed_stream_tag
31
+ # since we set the turbo request id before the build of the fetch request, Turbo will set
32
+ # the X-TURBO-REQUEST-ID header to a comma-separated list of request ids
33
+ # we only want to use the first request id (ours), so we split the header value and take the first part
34
+ safe_join [
35
+ tag.meta(name: 'turbo-request-id', content: ::Turbo.current_request_id.split(',').first),
36
+ tag.meta(name: 'turbo-delayed-stream', content: 'true')
37
+ ], "\n"
38
+ end
39
+
40
+ ## Editor helpers
29
41
  def maglev_editor_title
30
42
  case maglev_config.title
31
43
  when nil
@@ -139,9 +151,15 @@ module Maglev
139
151
  disappear_after: 3.seconds).with_content(message)
140
152
  end
141
153
 
142
- def maglev_page_icon(page, size: '1.15rem')
154
+ def maglev_page_icon(page, size: '1.15rem', wrapper_class_names: nil)
143
155
  icon_name = page.index? ? 'home' : 'file'
144
- render Maglev::Uikit::IconComponent.new(name: icon_name, size: size, class_names: 'shrink-0')
156
+ content_tag :span, class: class_names('shrink-0 relative', wrapper_class_names) do
157
+ if page.need_to_be_published?
158
+ concat(content_tag(:span, '',
159
+ class: 'absolute -bottom-0.25 right-0 bg-yellow-600 rounded-full w-1.5 h-1.5'))
160
+ end
161
+ concat render(Maglev::Uikit::IconComponent.new(name: icon_name, size: size))
162
+ end
145
163
  end
146
164
 
147
165
  def maglev_page_preview_reload_data
@@ -10,6 +10,7 @@
10
10
  # og_description_translations :jsonb
11
11
  # og_image_url_translations :jsonb
12
12
  # og_title_translations :jsonb
13
+ # published_at :datetime
13
14
  # sections_translations :jsonb
14
15
  # seo_title_translations :jsonb
15
16
  # title_translations :jsonb
@@ -55,6 +56,19 @@ module Maglev
55
56
  false
56
57
  end
57
58
 
59
+ def published?
60
+ published_at.present?
61
+ end
62
+
63
+ def need_to_be_published?
64
+ !published? || updated_at.blank? || updated_at > published_at
65
+ end
66
+
67
+ # opposite of #need_to_be_published?
68
+ def published_and_up_to_date?
69
+ published? && updated_at <= published_at
70
+ end
71
+
58
72
  def translate_in(locale, source_locale)
59
73
  %i[title sections seo_title meta_description og_title og_description og_image_url].each do |attr|
60
74
  translate_attr_in(attr, locale, source_locale)
@@ -8,6 +8,7 @@
8
8
  # locales :jsonb
9
9
  # lock_version :integer
10
10
  # name :string
11
+ # published_at :datetime
11
12
  # sections_translations :jsonb
12
13
  # style :jsonb
13
14
  # created_at :datetime not null
@@ -35,6 +36,10 @@ module Maglev
35
36
 
36
37
  ## methods ##
37
38
 
39
+ def published?
40
+ published_at.present?
41
+ end
42
+
38
43
  def api_attributes
39
44
  %i[id name]
40
45
  end
@@ -14,7 +14,9 @@ module Maglev
14
14
  private
15
15
 
16
16
  def site
17
- @site ||= Maglev::Site.first
17
+ @site ||= Maglev::Site.first.tap do |site|
18
+ raise Maglev::Errors::SiteNotFound unless site
19
+ end
18
20
  end
19
21
 
20
22
  def change_default_locales(site)
@@ -15,7 +15,7 @@ module Maglev
15
15
  argument :locale, default: nil
16
16
 
17
17
  def call
18
- fetch_container_store(page).sections.map do |section|
18
+ (fetch_container_store(page).sections || []).map do |section|
19
19
  transform_section(section.dup)
20
20
  end.compact
21
21
  end
@@ -21,6 +21,9 @@ module Maglev
21
21
  store = find_or_build_published_store(container)
22
22
  store.sections_translations = container.sections_translations
23
23
  store.save!
24
+ # mark the container as published.
25
+ # We need to add a delay to ensure that published_at will be posterior to the native updated_at of the container.
26
+ container.update(published_at: Time.current + 0.2.seconds)
24
27
  end
25
28
 
26
29
  def find_or_build_published_store(container)
@@ -18,7 +18,8 @@
18
18
  icon: 'drop',
19
19
  active: controller_name == 'style',
20
20
  options: {
21
- icon_size: '1.25rem'
21
+ icon_size: '1.25rem',
22
+ data: { controller: 'prevent-same-path' }
22
23
  }
23
24
  ) if maglev_theme.style? %>
24
25
 
@@ -4,17 +4,9 @@
4
4
  %>
5
5
 
6
6
  <% topbar.with_page_info do %>
7
- <%= render Maglev::Uikit::AppLayout::Topbar::PageInfoComponent.new(
8
- page: current_maglev_page,
9
- paths: {
10
- edit: edit_editor_page_path(current_maglev_page, maglev_editing_route_context),
11
- preview: current_maglev_page_urls[:preview],
12
- clone: editor_page_clone_path(current_maglev_page, maglev_editing_route_context),
13
- delete: editor_page_path(current_maglev_page, maglev_editing_route_context)
14
- },
15
- live_page_url: current_maglev_page_urls[:live],
16
- prefix_page_path: default_content_locale? ? '' : "#{content_locale}/"
17
- ) %>
7
+ <%= turbo_frame_tag dom_id(current_maglev_page, 'topbar-page-info'), data: { turbo_sync: true } do %>
8
+ <%= render 'layouts/maglev/editor/topbar/page_info' %>
9
+ <% end %>
18
10
  <% end %>
19
11
 
20
12
  <% topbar.with_actions do %>
@@ -30,16 +22,7 @@
30
22
  current_locale: Maglev::I18n.current_locale,
31
23
  ) if maglev_site.many_locales? %>
32
24
 
33
- <div class="flex items-center h-full px-4">
34
- <%= button_to editor_publication_path(maglev_editing_route_context),
35
- method: :post,
36
- class: maglev_button_classes(color: :primary, size: :medium),
37
- form_class: 'group/form is-default',
38
- data: { controller: 'submit-button' } do %>
39
- <%= maglev_button_label(
40
- t('maglev.editor.header_nav.publish_button.default'),
41
- pending: t('maglev.editor.header_nav.publish_button.in_progress'),
42
- )%>
43
- <% end %>
44
- </div>
25
+ <%= turbo_frame_tag dom_id(current_maglev_page, 'topbar-publish-button'), data: { turbo_sync: true } do %>
26
+ <%= render 'layouts/maglev/editor/topbar/publish_button' %>
27
+ <% end %>
45
28
  <% end %>
@@ -3,6 +3,7 @@
3
3
  <title><%= maglev_editor_title %></title>
4
4
  <meta name="view-transition" content="same-origin" />
5
5
  <meta name="turbo-refresh-method" content="morph">
6
+ <meta name="turbo-body" content="root">
6
7
 
7
8
  <meta name="content-locale" content="<%= content_locale %>">
8
9
  <meta name="page-preview-url" content="<%= current_maglev_page_urls[:preview] %>">
@@ -0,0 +1,11 @@
1
+ <%= render Maglev::Uikit::AppLayout::Topbar::PageInfoComponent.new(
2
+ page: current_maglev_page,
3
+ paths: {
4
+ edit: edit_editor_page_path(current_maglev_page, maglev_editing_route_context),
5
+ preview: current_maglev_page_urls[:preview],
6
+ clone: editor_page_clone_path(current_maglev_page, maglev_editing_route_context),
7
+ delete: editor_page_path(current_maglev_page, maglev_editing_route_context)
8
+ },
9
+ live_page_url: current_maglev_page_urls[:live],
10
+ prefix_page_path: default_content_locale? ? '' : "#{content_locale}/"
11
+ ) %>
@@ -0,0 +1,13 @@
1
+ <div class="flex items-center h-full px-4">
2
+ <%= button_to editor_publication_path(maglev_editing_route_context),
3
+ method: :post,
4
+ class: maglev_button_classes(color: :primary, size: :medium),
5
+ form_class: 'group/form is-default',
6
+ disabled: current_maglev_page.published_and_up_to_date?,
7
+ data: { controller: 'submit-button' } do %>
8
+ <%= maglev_button_label(
9
+ t('maglev.editor.header_nav.publish_button.default'),
10
+ pending: t('maglev.editor.header_nav.publish_button.in_progress'),
11
+ )%>
12
+ <% end %>
13
+ </div>
@@ -1 +1,3 @@
1
- <%= turbo_stream.console_log 'Publication created' %>
1
+ <%= turbo_stream.console_log 'Publication created' %>
2
+
3
+ <%= turbo_stream.refresh delayed: true %>