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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29cb78070d1a03bb67b5c2ad9c06b212303ae2c390e3b3a2f17b07bbae26f949
4
- data.tar.gz: 82dd11dac19e77c935a037561d668be95b6d4cc0a6cb08d53ce1e4eec81445b4
3
+ metadata.gz: '094c93e8f96929b8abc542b096f60de09e69165f3158c3b17a1bedd0815b3645'
4
+ data.tar.gz: 77ddc4feb76c37b9d1f6bb5f13230a18cc452a30d4435aee38188fb93853c74d
5
5
  SHA512:
6
- metadata.gz: e07ea70a61268187c94a66ad273339bed785d0cc3e6f5562624b4924b6e721ff6f01d039e47a1ca9f18de58681b051ac50bbc03bc1ceeefa79dd351f94ff8250
7
- data.tar.gz: b39a29416a88c3cace9b780a4a0582b721b5d038df0c0e7b14d6814b48612db14530c8e6bc8259a7fdca30febd8a70e8a290a3fb9ad33b6e4289333c4556e8ad
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
- format.json { render json: { message: t('lato_cms.fields_saved'), fields: @page.fields.reload.map(&:as_json) } }
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
- Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true) if file.attached?
157
+ attachment_url(file) if file.attached?
158
158
  end
159
159
 
160
160
  def poster_url
161
- Rails.application.routes.url_helpers.rails_blob_path(poster_file, only_path: true) if poster_file.attached?
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
- Rails.application.routes.url_helpers.rails_representation_path(
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
- Rails.application.routes.url_helpers.rails_representation_path(
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] = url_helpers.rails_representation_path(file.variant(transformation), only_path: true)
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 %>
@@ -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, :llm_api_url, :llm_model, :llm_api_key,
7
- :llm_generate_alt_text, :llm_generate_title, :llm_alt_text_prompt, :llm_title_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
@@ -1,3 +1,3 @@
1
1
  module LatoCms
2
- VERSION = "3.2.5"
2
+ VERSION = "3.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lato_cms
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.5
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregorio Galante