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.
- checksums.yaml +4 -4
- data/app/assets/builds/maglev/tailwind.css +523 -11852
- data/app/assets/javascripts/maglev/editor/controllers/app/preview_notification_center_controller.js +1 -1
- data/app/assets/javascripts/maglev/editor/controllers/shared/submit_button_controller.js +29 -5
- data/app/assets/javascripts/maglev/editor/controllers/utils.js +16 -0
- data/app/assets/javascripts/maglev/editor/index.js +3 -43
- data/app/assets/javascripts/maglev/editor/patches/page_renderer_patch.js +47 -0
- data/app/assets/javascripts/maglev/editor/patches/turbo_delayed_streams.js +35 -0
- data/app/assets/javascripts/maglev/editor/patches/turbo_stream_patch.js +50 -0
- data/app/assets/stylesheets/maglev/application.css +0 -2
- data/app/assets/stylesheets/maglev/tailwind.css.erb +3 -1
- data/app/components/maglev/uikit/app_layout/topbar/logo_component.html.erb +1 -1
- data/app/components/maglev/uikit/app_layout/topbar/page_info_component.html.erb +1 -1
- data/app/components/maglev/uikit/dropdown_component/dropdown_controller.js +6 -1
- data/app/controllers/concerns/maglev/editor/errors_concern.rb +9 -9
- data/app/controllers/concerns/maglev/errors_concern.rb +17 -0
- data/app/controllers/maglev/application_controller.rb +1 -0
- data/app/controllers/maglev/site_controller.rb +15 -0
- data/app/helpers/maglev/application_helper.rb +20 -2
- data/app/models/maglev/page.rb +14 -0
- data/app/models/maglev/site.rb +5 -0
- data/app/services/maglev/fetch_site.rb +3 -1
- data/app/services/maglev/get_published_page_sections_service.rb +1 -1
- data/app/services/maglev/publish_service.rb +3 -0
- data/app/views/layouts/maglev/editor/_sidebar.html.erb +2 -1
- data/app/views/layouts/maglev/editor/_topbar.html.erb +6 -23
- data/app/views/layouts/maglev/editor/application.html.erb +1 -0
- data/app/views/layouts/maglev/editor/topbar/_page_info.html.erb +11 -0
- data/app/views/layouts/maglev/editor/topbar/_publish_button.html.erb +13 -0
- data/app/views/maglev/editor/publication/create.turbo_stream.erb +3 -1
- data/app/views/maglev/editor/section_blocks/update.turbo_stream.erb +9 -1
- data/app/views/maglev/editor/sections/update.turbo_stream.erb +9 -1
- data/app/views/maglev/errors/site_not_found.html.erb +33 -0
- data/config/editor_importmap.rb +3 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20251116171603_add_published_at_to_sites_and_pages.rb +6 -0
- data/lib/maglev/engine.rb +1 -0
- data/lib/maglev/errors.rb +1 -0
- data/lib/maglev/version.rb +1 -1
- data/lib/tasks/maglev/tailwindcss.rake +1 -0
- metadata +10 -1
|
@@ -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
|
-
|
|
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 <
|
|
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(
|
|
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
|
|
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
|
+
})
|
|
@@ -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
|
-
<%=
|
|
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
|
|
@@ -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
|
-
|
|
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
|
data/app/models/maglev/page.rb
CHANGED
|
@@ -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)
|
data/app/models/maglev/site.rb
CHANGED
|
@@ -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
|
|
@@ -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)
|
|
@@ -4,17 +4,9 @@
|
|
|
4
4
|
%>
|
|
5
5
|
|
|
6
6
|
<% topbar.with_page_info do %>
|
|
7
|
-
<%=
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
34
|
-
<%=
|
|
35
|
-
|
|
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>
|