lato_cms 3.2.5 → 3.3.0
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/README.md +25 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_page_preview_controller.js +13 -0
- data/app/controllers/lato_cms/pages_controller.rb +6 -1
- data/app/models/lato_cms/media.rb +21 -10
- data/app/views/lato_cms/pages/_component_accordion.html.erb +11 -1
- data/lib/lato_cms/config.rb +22 -2
- data/lib/lato_cms/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '094c93e8f96929b8abc542b096f60de09e69165f3158c3b17a1bedd0815b3645'
|
|
4
|
+
data.tar.gz: 77ddc4feb76c37b9d1f6bb5f13230a18cc452a30d4435aee38188fb93853c74d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91438e0eba689387d221a7f27ceb24fae3b249a3f154e5fff1ceca4f7812242e9278488e247c0f59adad09b30ab5ca02b383b90dc05a8ed3528f32449dc798c2
|
|
7
|
+
data.tar.gz: 85b62b2734785e61ee3e4b88f74a2b487d6fd6bd6cd533f9a39dfe26bef85b7971dfbe8dc34b9de57c139bc5ee5c8889c8e2b43495e3799049a135cd9540a747
|
data/README.md
CHANGED
|
@@ -43,6 +43,31 @@ import "lato_cms/application";
|
|
|
43
43
|
// ....
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
## Media URLs
|
|
47
|
+
|
|
48
|
+
By default a media URL is an Active Storage **redirect** URL: Rails answers 302 with a
|
|
49
|
+
signed address that expires. On a remote service (S3 and friends) that is what you want —
|
|
50
|
+
the file then comes from the service itself.
|
|
51
|
+
|
|
52
|
+
On a **disk** service it is the wrong trade: the redirect costs a second request, and its
|
|
53
|
+
target is served `private, must-revalidate`, so no browser, proxy or CDN can keep it. A
|
|
54
|
+
video of a few dozen MB is then downloaded again on every visit, from a thread of your app
|
|
55
|
+
server. Switch the mode:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
LatoCms.configure do |config|
|
|
59
|
+
config.media_url_mode = :proxy
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Media URLs (the blob and every variant) then point at Active Storage's proxy controller:
|
|
64
|
+
no redirect, and `Cache-Control: public, immutable`, which a cache in front of the app can
|
|
65
|
+
actually use. The address carries the signature of the blob, so a replaced file is a
|
|
66
|
+
different address and can never be served stale.
|
|
67
|
+
|
|
68
|
+
Both modes ask for an `inline` disposition: a media of the library is content to show, and
|
|
69
|
+
the default for a video would otherwise turn opening its address into a download.
|
|
70
|
+
|
|
46
71
|
## Development
|
|
47
72
|
|
|
48
73
|
Clone repository, install dependencies, run migrations and start:
|
|
@@ -6,8 +6,16 @@ export default class extends Controller {
|
|
|
6
6
|
connect () {
|
|
7
7
|
this.handleComponentChange = this.handleComponentChange.bind(this)
|
|
8
8
|
this.handleIframeLoad = this.handleIframeLoad.bind(this)
|
|
9
|
+
this.handleMorph = this.handleMorph.bind(this)
|
|
9
10
|
|
|
10
11
|
document.addEventListener('lato-cms:component-change', this.handleComponentChange)
|
|
12
|
+
// Actions that re-render the editor server-side (cloning a component from
|
|
13
|
+
// another locale, editing the page settings) redirect back to this same
|
|
14
|
+
// URL, which Turbo treats as a page refresh and applies by morphing. Morph
|
|
15
|
+
// leaves an unchanged <iframe> exactly as it is — the element is never
|
|
16
|
+
// recreated, so the preview kept showing pre-clone content until a manual
|
|
17
|
+
// reload. The refresh is asked for explicitly here.
|
|
18
|
+
document.addEventListener('turbo:morph', this.handleMorph)
|
|
11
19
|
|
|
12
20
|
if (this.hasIframeTarget) {
|
|
13
21
|
this.iframeTarget.addEventListener('load', this.handleIframeLoad)
|
|
@@ -26,6 +34,7 @@ export default class extends Controller {
|
|
|
26
34
|
|
|
27
35
|
disconnect () {
|
|
28
36
|
document.removeEventListener('lato-cms:component-change', this.handleComponentChange)
|
|
37
|
+
document.removeEventListener('turbo:morph', this.handleMorph)
|
|
29
38
|
|
|
30
39
|
if (this.hasIframeTarget) {
|
|
31
40
|
this.iframeTarget.removeEventListener('load', this.handleIframeLoad)
|
|
@@ -109,6 +118,10 @@ export default class extends Controller {
|
|
|
109
118
|
this.postActiveComponent()
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
handleMorph () {
|
|
122
|
+
this.refresh()
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
syncInitiallyOpenComponent () {
|
|
113
126
|
const openCollapse = document.querySelector('#components-accordion .accordion-collapse.show')
|
|
114
127
|
if (!openCollapse) return
|
|
@@ -114,7 +114,12 @@ module LatoCms
|
|
|
114
114
|
respond_to do |format|
|
|
115
115
|
if errors.empty?
|
|
116
116
|
format.html { redirect_to lato_cms.pages_show_path(@page), notice: t('lato_cms.fields_saved') }
|
|
117
|
-
|
|
117
|
+
# Only the saved component's fields: the editor repaints its media
|
|
118
|
+
# fields from this list, matching them by persisted field id, which is
|
|
119
|
+
# unique within a component but not across the page (two components
|
|
120
|
+
# can both have an "image" field). Handing it the whole page let a
|
|
121
|
+
# field adopt another component's media, and the next save stored it.
|
|
122
|
+
format.json { render json: { message: t('lato_cms.fields_saved'), fields: @page.fields.reload.select { |field| field.template_component_id == template_component_id.to_s }.map(&:as_json) } }
|
|
118
123
|
else
|
|
119
124
|
error_messages = errors.map { |e| "#{e[:field_id]}: #{e[:errors].join(', ')}" }.join('; ')
|
|
120
125
|
format.html { redirect_to lato_cms.pages_show_path(@page), alert: error_messages }
|
|
@@ -154,11 +154,27 @@ module LatoCms
|
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
def url
|
|
157
|
-
|
|
157
|
+
attachment_url(file) if file.attached?
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
def poster_url
|
|
161
|
-
|
|
161
|
+
attachment_url(poster_file) if poster_file.attached?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The URL of an attachment or of one of its variants, in the mode the host
|
|
165
|
+
# app asked for (see LatoCms::Config::MEDIA_URL_MODES).
|
|
166
|
+
#
|
|
167
|
+
# `inline` in both modes: a media of the library is content to show — a
|
|
168
|
+
# picture in a page, a video in a player — and the default disposition of a
|
|
169
|
+
# video is `attachment`, which turns opening its address into a download.
|
|
170
|
+
# Whoever wants a download asks for it explicitly, as the panel does.
|
|
171
|
+
def attachment_url(attachable)
|
|
172
|
+
helpers = Rails.application.routes.url_helpers
|
|
173
|
+
if LatoCms.config.media_url_proxy?
|
|
174
|
+
helpers.rails_storage_proxy_path(attachable, only_path: true, disposition: :inline)
|
|
175
|
+
else
|
|
176
|
+
helpers.rails_blob_path(attachable, only_path: true, disposition: :inline)
|
|
177
|
+
end
|
|
162
178
|
end
|
|
163
179
|
|
|
164
180
|
# Swaps the underlying file while keeping the same record, so every field
|
|
@@ -248,9 +264,7 @@ module LatoCms
|
|
|
248
264
|
def thumbnail_url
|
|
249
265
|
return nil unless image? && file.attached? && file.variable?
|
|
250
266
|
|
|
251
|
-
|
|
252
|
-
file.variant(resize_to_fill: [200, 200]), only_path: true
|
|
253
|
-
)
|
|
267
|
+
attachment_url(file.variant(resize_to_fill: [200, 200]))
|
|
254
268
|
rescue StandardError => e
|
|
255
269
|
Rails.logger.error("LatoCms: Failed to build thumbnail for media #{id}: #{e.message}")
|
|
256
270
|
nil
|
|
@@ -263,9 +277,7 @@ module LatoCms
|
|
|
263
277
|
return nil unless image? && file.attached?
|
|
264
278
|
return url unless file.variable?
|
|
265
279
|
|
|
266
|
-
|
|
267
|
-
file.variant(resize_to_limit: [1200, 1200]), only_path: true
|
|
268
|
-
)
|
|
280
|
+
attachment_url(file.variant(resize_to_limit: [1200, 1200]))
|
|
269
281
|
rescue StandardError => e
|
|
270
282
|
Rails.logger.error("LatoCms: Failed to build preview for media #{id}: #{e.message}")
|
|
271
283
|
url
|
|
@@ -278,12 +290,11 @@ module LatoCms
|
|
|
278
290
|
def variant_urls(sizes_config)
|
|
279
291
|
return {} if sizes_config.blank? || !sizes_config.respond_to?(:each_pair) || !file.attached? || !file.variable?
|
|
280
292
|
|
|
281
|
-
url_helpers = Rails.application.routes.url_helpers
|
|
282
293
|
sizes_config.each_with_object({}) do |(name, opts), acc|
|
|
283
294
|
transformation = self.class.variant_transformation(opts)
|
|
284
295
|
next if transformation.blank?
|
|
285
296
|
|
|
286
|
-
acc[name] =
|
|
297
|
+
acc[name] = attachment_url(file.variant(transformation))
|
|
287
298
|
end
|
|
288
299
|
rescue StandardError => e
|
|
289
300
|
Rails.logger.error("LatoCms: Failed to build image variants for media #{id}: #{e.message}")
|
|
@@ -104,10 +104,20 @@
|
|
|
104
104
|
<% page_field = component_fields.find { |f| f.field_id == field_id } %>
|
|
105
105
|
|
|
106
106
|
<div class="mb-3">
|
|
107
|
+
<%# The dom id prefix carries the template component: every
|
|
108
|
+
component of the page is rendered at once, and two of them
|
|
109
|
+
can expose the same field id (a template mounting one
|
|
110
|
+
component twice, or two components both calling their
|
|
111
|
+
image field "image"). Without it they shared input ids —
|
|
112
|
+
and, for media fields, the picker frame id the selection
|
|
113
|
+
event is correlated by, so a media picked in one component
|
|
114
|
+
landed in the other as well. Input *names* stay
|
|
115
|
+
component-agnostic: each form posts its own component. %>
|
|
107
116
|
<%= lato_cms_render_field(
|
|
108
117
|
field_id: field_id,
|
|
109
118
|
field_config: field_config,
|
|
110
|
-
page_field: page_field
|
|
119
|
+
page_field: page_field,
|
|
120
|
+
dom_id_prefix: "fields_#{tc[:template_component_id]}_#{field_id}"
|
|
111
121
|
) %>
|
|
112
122
|
</div>
|
|
113
123
|
<% end %>
|
data/lib/lato_cms/config.rb
CHANGED
|
@@ -3,8 +3,19 @@ module LatoCms
|
|
|
3
3
|
# This class contains the default configuration of the engine.
|
|
4
4
|
##
|
|
5
5
|
class Config
|
|
6
|
-
attr_accessor :locales, :templates_path, :admin_roles, :
|
|
7
|
-
:llm_generate_alt_text, :llm_generate_title, :llm_alt_text_prompt,
|
|
6
|
+
attr_accessor :locales, :templates_path, :admin_roles, :media_url_mode, :llm_api_url, :llm_model,
|
|
7
|
+
:llm_api_key, :llm_generate_alt_text, :llm_generate_title, :llm_alt_text_prompt,
|
|
8
|
+
:llm_title_prompt
|
|
9
|
+
|
|
10
|
+
# How the URLs of media files are built (see LatoCms::Media#url).
|
|
11
|
+
# - :redirect — Rails answers 302 with a signed URL that expires; on a
|
|
12
|
+
# remote service (S3 and friends) the file then comes from the service
|
|
13
|
+
# itself, which is what you want there.
|
|
14
|
+
# - :proxy — the file is served through the app, with no redirect and with
|
|
15
|
+
# `Cache-Control: public, immutable`, so browsers and any cache in front
|
|
16
|
+
# can keep it. On a disk service this is strictly better: the redirect
|
|
17
|
+
# costs a second request and its target can be cached by nobody.
|
|
18
|
+
MEDIA_URL_MODES = %i[redirect proxy].freeze
|
|
8
19
|
|
|
9
20
|
# Placeholders substituted at request time in both default and custom LLM
|
|
10
21
|
# prompts (see LatoCms::Media#llm_prompt):
|
|
@@ -37,6 +48,11 @@ module LatoCms
|
|
|
37
48
|
# (create, update, delete) and translation links.
|
|
38
49
|
@admin_roles = { none: 0, operator: 1, admin: 2 }
|
|
39
50
|
|
|
51
|
+
# Default is :redirect, which is how it always worked: switching the way
|
|
52
|
+
# every media URL is built is a decision for the host app, not something
|
|
53
|
+
# an upgrade should do on its own. See MEDIA_URL_MODES.
|
|
54
|
+
@media_url_mode = :redirect
|
|
55
|
+
|
|
40
56
|
# Optional OpenAI-compatible endpoint used to auto-generate alt text and
|
|
41
57
|
# title for uploaded images (see LatoCms::Media#generate_text!). All
|
|
42
58
|
# three must be set for the feature to activate; unset by default.
|
|
@@ -56,6 +72,10 @@ module LatoCms
|
|
|
56
72
|
@llm_title_prompt = nil
|
|
57
73
|
end
|
|
58
74
|
|
|
75
|
+
def media_url_proxy?
|
|
76
|
+
media_url_mode.to_sym == :proxy
|
|
77
|
+
end
|
|
78
|
+
|
|
59
79
|
def llm_configured?
|
|
60
80
|
llm_api_url.present? && llm_model.present? && llm_api_key.present?
|
|
61
81
|
end
|
data/lib/lato_cms/version.rb
CHANGED