lato_cms 3.2.1 → 3.2.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: 33e3be18772b49e7523f66ee62e85d74ca3cbdaf6e6b0f9f39e0abeeb48dbea4
4
- data.tar.gz: e32a97e06e949782083f87f951cba818c9372f17169703eec90cbb741cabba94
3
+ metadata.gz: c972b24f2fdfbdd342a2b106ece261073d0b4517a1df42484863c9a38104920d
4
+ data.tar.gz: 3f1f14ba2ef176bbd09bf1073a74fcbe50d4b66c9997ae985ab578b364700ece
5
5
  SHA512:
6
- metadata.gz: f7ec56fd1e21c2ac94d1c4fee8cac7575f1f09eddc045f64c3f39aa603ad72d07f9f4ed0799edec2b1cc734d8dc690e389f8e374eae4d50436521cd4fd5ad4ac
7
- data.tar.gz: 217698bd6d223e2abfee2c937cffb2e380b5340b5f60c548600a624bb0ad8d92c689f1e14b188c71c2fd754088c5db4bb568921efa98b7ff9ef33efb7270a182
6
+ metadata.gz: db07c23bf8a9515cc43c05dd14a86fc405511f43c4b027eb5ee75837fb5c7a022597e63dc385cff96311c904f254811aae155e90f02000b6771874c4319ec932
7
+ data.tar.gz: 526c40a70c8b222c7d4499c4e977966810af2e1b55cce4b3b64800870755264a04cea9d6a0e6c97768817eaa1e08a4402a684ec7183fcce66d1dcbad0c159f4b
@@ -6,7 +6,7 @@ import { Controller } from '@hotwired/stimulus'
6
6
  // `lato-cms:media-selected` event on `document`, correlated by the picker's
7
7
  // own turbo-frame id (unique per field instance, even inside a repeater).
8
8
  export default class extends Controller {
9
- static targets = ['grid', 'item', 'selectedCount', 'confirmButton', 'uploadForm', 'uploadNotice']
9
+ static targets = ['grid', 'item', 'selectedCount', 'confirmButton', 'uploadNotice']
10
10
  static values = { multiple: Boolean, selectedLabel: String }
11
11
 
12
12
  connect () {
@@ -41,22 +41,11 @@ export default class extends Controller {
41
41
  this.closeModal()
42
42
  }
43
43
 
44
- async upload (event) {
45
- event.preventDefault()
46
-
47
- const form = event.currentTarget
48
- const response = await fetch(form.action, {
49
- method: 'POST',
50
- headers: { Accept: 'application/json' },
51
- body: new FormData(form)
52
- })
53
- const data = await response.json()
54
-
55
- if (!response.ok) {
56
- this.uploadNoticeTarget.innerHTML = `<div class="alert alert-danger">${Object.values(data).flat().join(', ')}</div>`
57
- return
58
- }
59
-
44
+ // Result of the XHR upload driven by `lato-cms-upload` (which owns the
45
+ // request and its progress bar); this only turns the created media into a
46
+ // selection.
47
+ uploadCompleted (event) {
48
+ const data = event.detail
60
49
  const item = {
61
50
  id: data.id,
62
51
  name: data.name,
@@ -74,10 +63,13 @@ export default class extends Controller {
74
63
 
75
64
  this.pending.set(item.id, item)
76
65
  this.updateSelectionUi()
77
- form.reset()
78
66
  this.uploadNoticeTarget.innerHTML = `<div class="alert alert-success">${item.name}</div>`
79
67
  }
80
68
 
69
+ uploadFailed (event) {
70
+ this.uploadNoticeTarget.innerHTML = `<div class="alert alert-danger">${event.detail.message}</div>`
71
+ }
72
+
81
73
  buildItemFromElement (element) {
82
74
  return {
83
75
  id: element.dataset.mediaId,
@@ -0,0 +1,91 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ // Submits a file form over XHR so the real upload progress can be shown.
4
+ // Neither of the two alternatives can: a plain form submit just freezes the
5
+ // page until the server answers, and `fetch` exposes no upload progress
6
+ // events at all (only download ones). Large videos made that gap obvious.
7
+ //
8
+ // On success it dispatches `lato-cms:upload-complete` on the form with the
9
+ // parsed JSON body (the media picker uses it to select the fresh media), or
10
+ // navigates to `redirectUrlValue` when the form is a standalone page.
11
+ export default class extends Controller {
12
+ static targets = ['progress', 'bar', 'label', 'submit', 'notice']
13
+ static values = { redirectUrl: String, processingLabel: String, confirm: String }
14
+
15
+ submit (event) {
16
+ // This handler owns the submission, so Turbo never sees it and
17
+ // `data-turbo-confirm` would never fire: the confirmation is asked here.
18
+ event.preventDefault()
19
+ if (this.confirmValue && !window.confirm(this.confirmValue)) return
20
+
21
+ const xhr = new window.XMLHttpRequest()
22
+ xhr.open('POST', this.element.action, true)
23
+ xhr.setRequestHeader('Accept', 'application/json')
24
+ xhr.upload.addEventListener('progress', (e) => {
25
+ if (e.lengthComputable) this.setProgress((e.loaded / e.total) * 100)
26
+ })
27
+ xhr.addEventListener('load', () => this.finish(xhr))
28
+ xhr.addEventListener('error', () => this.fail())
29
+
30
+ this.start()
31
+ xhr.send(new window.FormData(this.element))
32
+ }
33
+
34
+ start () {
35
+ this.clearNotice()
36
+ if (this.hasSubmitTarget) this.submitTarget.disabled = true
37
+ if (this.hasProgressTarget) this.progressTarget.classList.remove('d-none')
38
+ this.setProgress(0)
39
+ }
40
+
41
+ // Past 100% the bytes are in, but the server is still attaching and
42
+ // processing them: the bar stays full and animated, the label says so.
43
+ setProgress (percent) {
44
+ const value = Math.round(percent)
45
+ if (this.hasBarTarget) this.barTarget.style.width = `${value}%`
46
+ if (this.hasLabelTarget) this.labelTarget.textContent = value >= 100 ? this.processingLabelValue : `${value}%`
47
+ }
48
+
49
+ finish (xhr) {
50
+ const data = this.parse(xhr.responseText)
51
+
52
+ if (xhr.status < 200 || xhr.status >= 300) {
53
+ this.fail(data)
54
+ return
55
+ }
56
+
57
+ if (this.redirectUrlValue) {
58
+ window.Turbo.visit(this.redirectUrlValue)
59
+ return
60
+ }
61
+
62
+ this.reset()
63
+ this.element.reset()
64
+ this.element.dispatchEvent(new CustomEvent('lato-cms:upload-complete', { bubbles: true, detail: data }))
65
+ }
66
+
67
+ fail (data) {
68
+ this.reset()
69
+ const message = data ? Object.values(data).flat().join(', ') : ''
70
+ this.element.dispatchEvent(new CustomEvent('lato-cms:upload-error', { bubbles: true, detail: { message } }))
71
+ if (this.hasNoticeTarget) this.noticeTarget.innerHTML = `<div class="alert alert-danger">${message}</div>`
72
+ }
73
+
74
+ reset () {
75
+ if (this.hasSubmitTarget) this.submitTarget.disabled = false
76
+ if (this.hasProgressTarget) this.progressTarget.classList.add('d-none')
77
+ this.setProgress(0)
78
+ }
79
+
80
+ clearNotice () {
81
+ if (this.hasNoticeTarget) this.noticeTarget.innerHTML = ''
82
+ }
83
+
84
+ parse (body) {
85
+ try {
86
+ return JSON.parse(body)
87
+ } catch (err) {
88
+ return null
89
+ }
90
+ }
91
+ }
@@ -1,6 +1,6 @@
1
1
  module LatoCms
2
2
  class MediaController < ApplicationController
3
- ADMIN_ONLY_ACTIONS = %i[destroy_action].freeze
3
+ ADMIN_ONLY_ACTIONS = %i[replace_file_action destroy_action].freeze
4
4
 
5
5
  before_action { active_sidebar(:lato_cms_media) }
6
6
  before_action :authenticate_lato_cms_admin, only: ADMIN_ONLY_ACTIONS
@@ -8,7 +8,7 @@ module LatoCms
8
8
  def index
9
9
  @media = lato_index_collection(
10
10
  query_media.order(created_at: :desc),
11
- columns: %i[name media_type actions],
11
+ columns: %i[name media_type usages actions],
12
12
  sortable_columns: %i[name media_type created_at],
13
13
  searchable_columns: %i[name alt_text title],
14
14
  default_sort_by: 'created_at|DESC',
@@ -35,6 +35,12 @@ module LatoCms
35
35
 
36
36
  respond_to do |format|
37
37
  if @media.save
38
+ # The JSON branch serves XHR uploads (progress bar, see
39
+ # lato_cms_upload_controller.js). `notify` is sent only by forms that
40
+ # navigate somewhere afterwards, so the flash lands on that page; the
41
+ # media picker uploads in place and asks for no flash.
42
+ flash[:notice] = t('lato_cms.media_created') if params[:notify].present?
43
+
38
44
  format.html { redirect_to lato_cms.media_path, notice: t('lato_cms.media_created') }
39
45
  format.json { render json: @media }
40
46
  else
@@ -89,6 +95,34 @@ module LatoCms
89
95
  end
90
96
  end
91
97
 
98
+ # Replaces the file of an existing media in place, so every page already
99
+ # using it picks up the new file. Kept out of `update_action` (and off
100
+ # `update_params`) on purpose: this is a destructive, admin-only edit to
101
+ # every usage at once, not a metadata change.
102
+ def replace_file_action
103
+ @media = query_media.find(params[:id])
104
+ file = params.dig(:media, :file)
105
+
106
+ respond_to do |format|
107
+ if file.present? && @media.replace_file!(file)
108
+ format.html { redirect_to lato_cms.media_update_path(@media), notice: t('lato_cms.media_file_replaced') }
109
+ format.json { render json: @media }
110
+ else
111
+ message = t('lato_cms.media_file_replace_failed')
112
+ format.html { redirect_to lato_cms.media_update_path(@media), alert: message }
113
+ format.json { render json: { error: message }, status: :unprocessable_entity }
114
+ end
115
+ end
116
+ rescue StandardError => e
117
+ Rails.logger.error("LatoCms: failed to replace file for media #{params[:id]}: #{e.message}")
118
+ message = t('lato_cms.media_file_replace_failed')
119
+
120
+ respond_to do |format|
121
+ format.html { redirect_to lato_cms.media_update_path(@media), alert: message }
122
+ format.json { render json: { error: message }, status: :unprocessable_entity }
123
+ end
124
+ end
125
+
92
126
  def destroy_action
93
127
  @media = query_media.find(params[:id])
94
128
  in_use = @media.usage_count.positive?
@@ -112,10 +146,10 @@ module LatoCms
112
146
  params.require(:media).permit(:file, :name, :alt_text, :title)
113
147
  end
114
148
 
115
- # :file is intentionally not permitted here: the underlying file is
116
- # immutable once a Media exists (it can be reused by many fields across
117
- # many pages, so replacing it in place would silently change what renders
118
- # everywhere it's referenced). A different file means a new Media.
149
+ # :file is intentionally not permitted here: a media can be reused by many
150
+ # fields across many pages, so swapping its file silently changes what
151
+ # renders everywhere it's referenced. That swap is possible, but only
152
+ # through the explicit, admin-only `replace_file_action`.
119
153
  def update_params
120
154
  translation_keys = LatoCms::Media::TRANSLATABLE_ATTRIBUTES.product(LatoCms.config.locales).map { |attribute, locale| :"#{attribute}_#{locale}" }
121
155
  params.require(:media).permit(:name, *translation_keys)
@@ -30,6 +30,16 @@ module LatoCms
30
30
  end
31
31
  end
32
32
 
33
+ # Index usages cell: how many page fields reference this media, so the
34
+ # admin sees at a glance which files are actually in use (the detail is in
35
+ # the edit form, which lists the pages).
36
+ def lato_cms_media_usages(media)
37
+ count = media.usage_count
38
+ return content_tag(:span, t('lato_cms.media_usages_none'), class: 'text-muted small') if count.zero?
39
+
40
+ content_tag(:span, t('lato_cms.media_usages_count', count: count), class: 'badge bg-info text-dark')
41
+ end
42
+
33
43
  def lato_cms_media_media_type(media)
34
44
  content_tag(:span, media.media_type, class: 'badge bg-secondary')
35
45
  end
@@ -126,9 +126,18 @@ module LatoCms
126
126
  multiple ? "#{name}[]" : name
127
127
  end
128
128
 
129
+ # Sanitizes only: `parameterize` also downcased, which silently broke the
130
+ # repeater's "NEW_RECORD" placeholder. The repeater JS swaps that literal
131
+ # for a fresh uuid when an item is added (see
132
+ # lato_cms_repeater_controller#add), but it never reached the JS as
133
+ # "NEW_RECORD" — it arrived lowercased inside every dom id and, worse,
134
+ # inside each media field's picker frame id. Every item added in one
135
+ # session therefore shared those ids, and since the picker correlates its
136
+ # selection event by frame id, choosing a media for one item dropped it
137
+ # into all of them at once.
129
138
  def lato_cms_field_dom_id(field_id, suffix = 'value', dom_id_prefix: nil)
130
139
  base = dom_id_prefix.presence || "fields_#{field_id}"
131
- "#{base}_#{suffix}".parameterize(separator: '_')
140
+ "#{base}_#{suffix}".gsub(/[^a-zA-Z0-9_]/, '_')
132
141
  end
133
142
 
134
143
  private
@@ -78,6 +78,14 @@ module LatoCms
78
78
  page_field_media.count
79
79
  end
80
80
 
81
+ # Every page field referencing this media, grouped by page (sorted by page
82
+ # title). Drives the "used in" list in the admin: a media is shared, so
83
+ # both deleting it and replacing its file are edits to every page listed
84
+ # here, and the admin has to see that before doing either.
85
+ def usages
86
+ page_fields.includes(:page).group_by(&:page).sort_by { |page, _fields| page.title.to_s.downcase }
87
+ end
88
+
81
89
  def filename
82
90
  file.filename.to_s if file.attached?
83
91
  end
@@ -119,6 +127,22 @@ module LatoCms
119
127
  Rails.application.routes.url_helpers.rails_blob_path(poster_file, only_path: true) if poster_file.attached?
120
128
  end
121
129
 
130
+ # Swaps the underlying file while keeping the same record, so every field
131
+ # already referencing this media renders the new file: the point of the
132
+ # action (replace a logo everywhere at once) and its danger at the same
133
+ # time. Active Storage purges the previous blob on attach, the stale video
134
+ # poster is dropped explicitly, and media_type is re-inferred since the
135
+ # new file can be of a different kind. Variants need no cleanup: they are
136
+ # derived from the blob, so the old ones die with it.
137
+ def replace_file!(new_file)
138
+ file.attach(new_file)
139
+ poster_file.purge if poster_file.attached?
140
+ update!(media_type: self.class.infer_media_type(file.content_type))
141
+ enqueue_poster_generation if video?
142
+
143
+ true
144
+ end
145
+
122
146
  # Best effort: generates a poster from the video via Active Storage previews
123
147
  # (ffmpeg). Any failure is logged, the video keeps working without a poster.
124
148
  def generate_video_poster!
@@ -1,9 +1,21 @@
1
1
  <% media ||= LatoCms::Media.new %>
2
2
 
3
3
  <%= turbo_frame_tag dom_id(media, 'form') do %>
4
- <%= form_with model: media, url: lato_cms.media_create_action_path, data: { turbo_frame: '_self', controller: 'lato-form' } do |form| %>
4
+ <%# Submitted over XHR (see lato-cms-upload) to show upload progress; the
5
+ `notify` flag asks the JSON branch to leave a flash for the page we
6
+ navigate to once the upload is done. %>
7
+ <%= form_with model: media, url: lato_cms.media_create_action_path, data: {
8
+ turbo_frame: '_self',
9
+ controller: 'lato-form lato-cms-upload',
10
+ action: 'submit->lato-cms-upload#submit',
11
+ lato_cms_upload_redirect_url_value: lato_cms.media_path,
12
+ lato_cms_upload_processing_label_value: t('lato_cms.media_upload_processing')
13
+ } do |form| %>
5
14
  <%= lato_form_notices class: %w[mb-3] %>
6
15
  <%= lato_form_errors media, class: %w[mb-3] %>
16
+ <%= hidden_field_tag :notify, true %>
17
+
18
+ <div data-lato-cms-upload-target="notice"></div>
7
19
 
8
20
  <%# Name/alt text are edited afterwards from this same list's "edit"
9
21
  action — they default to the filename on upload, so there's nothing
@@ -13,8 +25,10 @@
13
25
  <%= lato_form_item_input_file_dropzone form, :file %>
14
26
  </div>
15
27
 
28
+ <%= render 'lato_cms/media/upload_progress' %>
29
+
16
30
  <div class="d-flex justify-content-end">
17
- <%= lato_form_submit form, t('lato_cms.media_create_cta'), class: %w[btn-success] %>
31
+ <%= lato_form_submit form, t('lato_cms.media_create_cta'), class: %w[btn-success], data: { lato_cms_upload_target: 'submit' } %>
18
32
  </div>
19
33
  <% end %>
20
34
  <% end %>
@@ -0,0 +1,32 @@
1
+ <%# Swaps the file behind an existing media: every page using it renders the
2
+ new file. Deliberately separate from the metadata form (different action,
3
+ admin only, destructive across every usage). %>
4
+ <div class="card border-warning mt-4">
5
+ <div class="card-header">
6
+ <h3 class="fs-6 mb-0"><%= t('lato_cms.media_replace_file_title') %></h3>
7
+ </div>
8
+ <div class="card-body">
9
+ <p class="text-muted small">
10
+ <%= t('lato_cms.media_replace_file_description', count: media.usage_count) %>
11
+ </p>
12
+
13
+ <%= form_with model: media, url: lato_cms.media_replace_file_action_path(media), method: :patch, data: {
14
+ controller: 'lato-cms-upload',
15
+ action: 'submit->lato-cms-upload#submit',
16
+ lato_cms_upload_redirect_url_value: lato_cms.media_update_path(media),
17
+ lato_cms_upload_processing_label_value: t('lato_cms.media_upload_processing'),
18
+ lato_cms_upload_confirm_value: t('lato_cms.media_replace_file_confirm')
19
+ } do |form| %>
20
+ <div data-lato-cms-upload-target="notice"></div>
21
+
22
+ <%= lato_form_item_input_file_dropzone form, :file %>
23
+ <%= render 'lato_cms/media/upload_progress' %>
24
+
25
+ <div class="d-flex justify-content-end">
26
+ <button type="submit" class="btn btn-warning" data-lato-cms-upload-target="submit">
27
+ <%= t('lato_cms.media_replace_file_cta') %>
28
+ </button>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+ </div>
@@ -35,8 +35,14 @@
35
35
  <% end %>
36
36
  <%= render "lato_cms/media/translations_field", form: form, media: media, attribute: "title" %>
37
37
 
38
+ <%= render 'lato_cms/media/usages', media: media %>
39
+
38
40
  <div class="d-flex justify-content-end">
39
41
  <%= lato_form_submit form, t('lato_cms.cta_update'), class: %w[btn-success] %>
40
42
  </div>
41
43
  <% end %>
44
+
45
+ <% if lato_cms_admin? %>
46
+ <%= render 'lato_cms/media/form_replace_file', media: media %>
47
+ <% end %>
42
48
  <% end %>
@@ -0,0 +1,8 @@
1
+ <%# Progress UI driven by lato_cms_upload_controller.js: hidden until a
2
+ submit starts, then fed the real bytes-sent percentage. %>
3
+ <div class="d-none my-3" data-lato-cms-upload-target="progress">
4
+ <div class="progress" style="height: 6px;">
5
+ <div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0%;" role="progressbar" data-lato-cms-upload-target="bar"></div>
6
+ </div>
7
+ <div class="small text-muted mt-1" data-lato-cms-upload-target="label">0%</div>
8
+ </div>
@@ -0,0 +1,24 @@
1
+ <%# Where this media is actually rendered: a media is shared, so this is the
2
+ blast radius of replacing its file or deleting it. %>
3
+ <% usages = media.usages %>
4
+
5
+ <div class="mb-3">
6
+ <label class="form-label"><%= t('lato_cms.media_usages_title') %></label>
7
+
8
+ <% if usages.empty? %>
9
+ <p class="text-muted small mb-0"><%= t('lato_cms.media_usages_empty') %></p>
10
+ <% else %>
11
+ <ul class="list-group list-group-flush border rounded">
12
+ <% usages.each do |page, fields| %>
13
+ <li class="list-group-item d-flex justify-content-between align-items-center gap-2">
14
+ <div>
15
+ <%= link_to page.title, lato_cms.pages_show_path(page), class: 'text-decoration-none' %>
16
+ <span class="badge bg-secondary ms-1"><%= page.locale %></span>
17
+ <div class="text-muted small"><%= fields.map(&:field_name).uniq.join(', ') %></div>
18
+ </div>
19
+ <%= link_to t('lato_cms.cta_edit'), lato_cms.pages_show_path(page), class: 'btn btn-sm btn-outline-secondary' %>
20
+ </li>
21
+ <% end %>
22
+ </ul>
23
+ <% end %>
24
+ </div>
@@ -84,12 +84,17 @@
84
84
  ActiveRecord::Relation delegates an unrecognized `#name` call to
85
85
  its model class, silently pre-filling a `:name` field with the
86
86
  literal string "LatoCms::Media" (bit us once already here). %>
87
+ <%# Uploaded over XHR by `lato-cms-upload` (progress bar) instead of a
88
+ plain submit; the picker only reacts to the result events. %>
87
89
  <%= form_with model: LatoCms::Media.new, url: lato_cms.media_create_action_path, data: {
88
- lato_cms_media_picker_target: 'uploadForm', action: 'submit->lato-cms-media-picker#upload'
90
+ controller: 'lato-cms-upload',
91
+ action: 'submit->lato-cms-upload#submit lato-cms:upload-complete->lato-cms-media-picker#uploadCompleted lato-cms:upload-error->lato-cms-media-picker#uploadFailed',
92
+ lato_cms_upload_processing_label_value: t('lato_cms.media_upload_processing')
89
93
  } do |form| %>
90
94
  <%= lato_form_item_input_file_dropzone form, :file, accept: { 'image' => 'image/*', 'video' => 'video/*' }[params[:type]] %>
95
+ <%= render 'lato_cms/media/upload_progress' %>
91
96
  <div class="d-flex justify-content-end">
92
- <button type="submit" class="btn btn-success"><%= t('lato_cms.media_upload_cta') %></button>
97
+ <button type="submit" class="btn btn-success" data-lato-cms-upload-target="submit"><%= t('lato_cms.media_upload_cta') %></button>
93
98
  </div>
94
99
  <% end %>
95
100
  </div>
@@ -3,6 +3,8 @@ en:
3
3
  attributes:
4
4
  lato/user:
5
5
  lato_cms_admin_role: Lato CMS role
6
+ lato_cms/media:
7
+ usages: Usages
6
8
  lato_cms:
7
9
  admin_roles:
8
10
  none: No access
@@ -121,6 +123,22 @@ en:
121
123
  media_picker_selected_label: selected
122
124
  media_search_placeholder: Search media...
123
125
  media_upload_cta: Upload
126
+ media_upload_processing: Processing...
127
+ media_usages_title: Used in
128
+ media_usages_empty: This media is not used in any page yet.
129
+ media_usages_none: Unused
130
+ media_usages_count:
131
+ one: "%{count} usage"
132
+ other: "%{count} usages"
133
+ media_replace_file_title: Replace file
134
+ media_replace_file_description:
135
+ zero: "Uploads a new file in place of the current one, keeping the same media record. This media is not used yet."
136
+ one: "Uploads a new file in place of the current one, keeping the same media record: the %{count} page field using it will show the new file."
137
+ other: "Uploads a new file in place of the current one, keeping the same media record: the %{count} page fields using it will show the new file."
138
+ media_replace_file_cta: Replace file
139
+ media_replace_file_confirm: "The current file will be replaced everywhere this media is used. Continue?"
140
+ media_file_replaced: File replaced successfully
141
+ media_file_replace_failed: Failed to replace the file
124
142
  media_type_all: All
125
143
  media_type_image: Images
126
144
  media_type_video: Videos
@@ -3,6 +3,8 @@ it:
3
3
  attributes:
4
4
  lato/user:
5
5
  lato_cms_admin_role: Ruolo Lato CMS
6
+ lato_cms/media:
7
+ usages: Utilizzi
6
8
  lato_cms:
7
9
  admin_roles:
8
10
  none: Nessun accesso
@@ -121,6 +123,22 @@ it:
121
123
  media_picker_selected_label: selezionati
122
124
  media_search_placeholder: Cerca media...
123
125
  media_upload_cta: Carica
126
+ media_upload_processing: Elaborazione...
127
+ media_usages_title: Utilizzato in
128
+ media_usages_empty: Questo media non è ancora utilizzato in nessuna pagina.
129
+ media_usages_none: Non utilizzato
130
+ media_usages_count:
131
+ one: "%{count} utilizzo"
132
+ other: "%{count} utilizzi"
133
+ media_replace_file_title: Sostituisci file
134
+ media_replace_file_description:
135
+ zero: "Carica un nuovo file al posto di quello attuale, mantenendo lo stesso media. Questo media non è ancora utilizzato."
136
+ one: "Carica un nuovo file al posto di quello attuale, mantenendo lo stesso media: il %{count} campo che lo utilizza mostrerà il nuovo file."
137
+ other: "Carica un nuovo file al posto di quello attuale, mantenendo lo stesso media: i %{count} campi che lo utilizzano mostreranno il nuovo file."
138
+ media_replace_file_cta: Sostituisci file
139
+ media_replace_file_confirm: "Il file attuale verrà sostituito ovunque questo media sia utilizzato. Continuare?"
140
+ media_file_replaced: File sostituito con successo
141
+ media_file_replace_failed: Impossibile sostituire il file
124
142
  media_type_all: Tutti
125
143
  media_type_image: Immagini
126
144
  media_type_video: Video
data/config/routes.rb CHANGED
@@ -32,6 +32,7 @@ LatoCms::Engine.routes.draw do
32
32
  post 'create', to: 'media#create_action', as: :media_create_action
33
33
  get ':id/update', to: 'media#update', as: :media_update
34
34
  patch ':id/update', to: 'media#update_action', as: :media_update_action
35
+ patch ':id/replace-file', to: 'media#replace_file_action', as: :media_replace_file_action
35
36
  post ':id/regenerate/:attribute', to: 'media#regenerate_text_action', as: :media_regenerate_text_action, constraints: { attribute: /alt_text|title/ }
36
37
  delete ':id', to: 'media#destroy_action', as: :media_destroy_action
37
38
  end
@@ -1,3 +1,3 @@
1
1
  module LatoCms
2
- VERSION = "3.2.1"
2
+ VERSION = "3.2.2"
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.1
4
+ version: 3.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregorio Galante
@@ -118,6 +118,7 @@ files:
118
118
  - app/assets/javascripts/lato_cms/controllers/lato_cms_reload_on_modal_close_controller.js
119
119
  - app/assets/javascripts/lato_cms/controllers/lato_cms_repeater_controller.js
120
120
  - app/assets/javascripts/lato_cms/controllers/lato_cms_text_field_controller.js
121
+ - app/assets/javascripts/lato_cms/controllers/lato_cms_upload_controller.js
121
122
  - app/assets/stylesheets/lato_cms/application.scss
122
123
  - app/controllers/lato_cms/api/pages_controller.rb
123
124
  - app/controllers/lato_cms/application_controller.rb
@@ -139,8 +140,11 @@ files:
139
140
  - app/models/lato_cms/page_field_media.rb
140
141
  - app/models/lato_cms/template_manager.rb
141
142
  - app/views/lato_cms/media/_form_create.html.erb
143
+ - app/views/lato_cms/media/_form_replace_file.html.erb
142
144
  - app/views/lato_cms/media/_form_update.html.erb
143
145
  - app/views/lato_cms/media/_translations_field.html.erb
146
+ - app/views/lato_cms/media/_upload_progress.html.erb
147
+ - app/views/lato_cms/media/_usages.html.erb
144
148
  - app/views/lato_cms/media/create.html.erb
145
149
  - app/views/lato_cms/media/index.html.erb
146
150
  - app/views/lato_cms/media/picker_action.html.erb