lato_cms 3.0.11 → 3.0.13

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: 18323c23906137ac104d760d833ca76cd64041f254372a8a7e037bee23a5d5c5
4
- data.tar.gz: 5324f43d1305e9aea41bcf19b2b41c96e555edd70ace601b788e649f5cb80b7d
3
+ metadata.gz: d1b3de7e40dab4ee5574179d52307473705c2b084aa7a2a76d03dcd0fb0f3cef
4
+ data.tar.gz: 70cfbb74343f62265a02f93d8998d92995b8332f8bc16e9671b3ab4af63ef70b
5
5
  SHA512:
6
- metadata.gz: d1c86cbe7a2a9dc66ffad24bb681f5a9022243c86f39ea4f2e8273c5500efec5f5ea20f610979a2f1bc49a4e85e96f1c5471faf00a5d42011b5df02da18cff5a
7
- data.tar.gz: eebc417e788f0275c9b6daf1e50777487008358f8b080b75b48cab6f192f1cea94d70f177da537f881ee723b90cf58d365bdcf51f197dc2f9dc0c88c072c9919
6
+ metadata.gz: '054983638f6a1454267a5d7eed3574152eac784a03ff1905c357a42bcd3fa5a3093b4591a8308008925f2b1612e7eb2eeccf2b58916a6080550156bc1414540d'
7
+ data.tar.gz: 1767c5b3550717035094aa78c755bc8b0531cbec89d430421334c91e9fe2b96b9d0c9b9c5735e52fdd582906b82846d55cce21002b7ec32ad8de6aaac034a415
@@ -27,6 +27,7 @@ export default class extends Controller {
27
27
 
28
28
  this.inputTarget.value = ''
29
29
  this.syncFileInput()
30
+ this.syncRequired()
30
31
  }
31
32
 
32
33
  remove(event) {
@@ -38,12 +39,14 @@ export default class extends Controller {
38
39
  item.remove()
39
40
  this.syncFileInput()
40
41
  if (!this.multipleValue) this.restoreReplacedItems()
42
+ this.syncRequired()
41
43
  return
42
44
  }
43
45
 
44
46
  item.classList.add('lato-cms-file-field__item--removing')
45
47
  item.querySelector('[data-lato-cms-file-field-target="removedNotice"]')?.classList.remove('d-none')
46
48
  this.ensureRemoveInput(item)
49
+ this.syncRequired()
47
50
  }
48
51
 
49
52
  undo(event) {
@@ -53,6 +56,19 @@ export default class extends Controller {
53
56
  item.classList.remove('lato-cms-file-field__item--removing')
54
57
  item.querySelector('[data-lato-cms-file-field-target="removedNotice"]')?.classList.add('d-none')
55
58
  item.querySelector('[data-remove-file-input]')?.remove()
59
+ this.syncRequired()
60
+ }
61
+
62
+ // The file input only has to be required while nothing survives the next save:
63
+ // a pending new file or a persisted attachment not marked for removal already
64
+ // satisfies the field, so keeping `required` would block every re-save (the
65
+ // form saves via fetch without reload, so the attribute must track state).
66
+ syncRequired() {
67
+ const hasActiveAttachment = this.newFileMap.size > 0 ||
68
+ this.itemTargets.some(item => {
69
+ return item.dataset.attachmentId && !item.classList.contains('lato-cms-file-field__item--removing')
70
+ })
71
+ this.inputTarget.required = this.fieldRequired && !hasActiveAttachment
56
72
  }
57
73
 
58
74
  ensureRemoveInput(item) {
@@ -124,6 +140,7 @@ export default class extends Controller {
124
140
  this.itemTargets
125
141
  .filter(item => item.querySelector('[data-remove-file-input]'))
126
142
  .forEach(item => item.remove())
143
+ this.syncRequired()
127
144
  return
128
145
  }
129
146
 
@@ -131,6 +148,7 @@ export default class extends Controller {
131
148
  field.attachments.forEach(attachment => {
132
149
  this.listTarget.appendChild(this.buildExistingItem(attachment))
133
150
  })
151
+ this.syncRequired()
134
152
  }
135
153
 
136
154
  findSavedField(fields) {
@@ -200,4 +218,8 @@ export default class extends Controller {
200
218
  get fieldId() {
201
219
  return this.element.dataset.fieldId
202
220
  }
221
+
222
+ get fieldRequired() {
223
+ return this.element.dataset.fieldRequired === 'true'
224
+ }
203
225
  }
@@ -38,6 +38,7 @@ export default class extends Controller {
38
38
  // Reset input then sync with DataTransfer
39
39
  this.fileInputTarget.value = ''
40
40
  this.syncFileInput()
41
+ this.syncRequired()
41
42
  }
42
43
 
43
44
  // ─── Remove ─────────────────────────────────────────────────────────────────
@@ -62,6 +63,7 @@ export default class extends Controller {
62
63
  item.remove()
63
64
  this.updateEmpty()
64
65
  this.updateOrderInputs()
66
+ this.syncRequired()
65
67
  }
66
68
 
67
69
  // ─── Drag & drop ─────────────────────────────────────────────────────────────
@@ -126,6 +128,17 @@ export default class extends Controller {
126
128
  this.emptyMsgTarget.classList.toggle('d-none', !!hasItems)
127
129
  }
128
130
 
131
+ // Required enforcement rides on native form validation: the file input is
132
+ // `visually-hidden` (not display:none) so the browser can focus it and anchor
133
+ // the validation bubble on the "Add images" button. `required` must be set
134
+ // only while no image survives the next save — pending files kept in the
135
+ // input via DataTransfer or surviving grid items satisfy the field.
136
+ syncRequired() {
137
+ const hasActiveItem = this.newFileMap.size > 0 ||
138
+ !!this.gridTarget.querySelector('[data-gallery-item]')
139
+ this.fileInputTarget.required = this.fieldRequired && !hasActiveItem
140
+ }
141
+
129
142
  buildItem(src, attachmentId, fileUid) {
130
143
  const div = document.createElement('div')
131
144
  div.setAttribute('data-gallery-item', '')
@@ -158,6 +171,7 @@ export default class extends Controller {
158
171
  })
159
172
  this.updateEmpty()
160
173
  this.updateOrderInputs()
174
+ this.syncRequired()
161
175
  }
162
176
 
163
177
  findSavedField(fields) {
@@ -169,4 +183,8 @@ export default class extends Controller {
169
183
  get removeLabel() {
170
184
  return this.element.dataset.removeLabel || 'Remove'
171
185
  }
186
+
187
+ get fieldRequired() {
188
+ return this.element.dataset.fieldRequired === 'true'
189
+ }
172
190
  }
@@ -45,6 +45,7 @@ export default class extends Controller {
45
45
  this.currentContainerTarget.classList.add('lato-cms-file-field__item--removing')
46
46
  this.removedNoticeTarget.classList.remove('d-none')
47
47
  this.ensureRemoveInput()
48
+ this.syncRequired()
48
49
  }
49
50
 
50
51
  undo() {
@@ -54,6 +55,16 @@ export default class extends Controller {
54
55
  this.removedNoticeTarget.classList.add('d-none')
55
56
  this.removeInput?.remove()
56
57
  this.removeInput = null
58
+ this.syncRequired()
59
+ }
60
+
61
+ // The file input only has to be required while no attachment survives the next
62
+ // save: a persisted attachment (not marked for removal) already satisfies the
63
+ // field, so keeping `required` on the emptied input would block every re-save.
64
+ syncRequired() {
65
+ const hasActiveAttachment = this.hasCurrentContainerTarget &&
66
+ !this.currentContainerTarget.classList.contains('lato-cms-file-field__item--removing')
67
+ this.inputTarget.required = this.fieldRequired && !hasActiveAttachment
57
68
  }
58
69
 
59
70
  ensureRemoveInput() {
@@ -82,6 +93,7 @@ export default class extends Controller {
82
93
  if (attachment) {
83
94
  this.element.insertBefore(this.buildCurrentContainer(attachment), this.newPreviewContainerTarget)
84
95
  }
96
+ this.syncRequired()
85
97
  }
86
98
 
87
99
  findSavedField(fields) {
@@ -139,6 +151,10 @@ export default class extends Controller {
139
151
  return this.element.dataset.fieldId
140
152
  }
141
153
 
154
+ get fieldRequired() {
155
+ return this.element.dataset.fieldRequired === 'true'
156
+ }
157
+
142
158
  get removeLabel() {
143
159
  return this.element.dataset.removeLabel || 'Remove'
144
160
  }
@@ -6,6 +6,44 @@ export default class extends Controller {
6
6
 
7
7
  connect () {
8
8
  this.refresh()
9
+
10
+ // Native validation skips hidden inputs: expand the item hosting an invalid
11
+ // field so the browser can focus it and show the error message.
12
+ this.onInvalid = (event) => this.expandItemFor(event.target)
13
+ this.element.addEventListener('invalid', this.onInvalid, true)
14
+ }
15
+
16
+ disconnect () {
17
+ this.element.removeEventListener('invalid', this.onInvalid, true)
18
+ }
19
+
20
+ expandItemFor (field) {
21
+ const item = field.closest('[data-lato-cms-repeater-target="item"]')
22
+ if (!item) return
23
+
24
+ this.collapseOthers(item)
25
+ this.setItemExpanded(item, true)
26
+ }
27
+
28
+ // Close every item except the given one (single-open accordion invariant).
29
+ collapseOthers (item) {
30
+ this.itemTargets.forEach((other) => {
31
+ if (other !== item) this.setItemExpanded(other, false)
32
+ })
33
+ }
34
+
35
+ // Bootstrap reads open state from the 'show' class, so toggling classes
36
+ // directly stays in sync with its lazily-created Collapse instances.
37
+ setItemExpanded (item, expanded) {
38
+ const collapse = item.querySelector('.collapse')
39
+ if (!collapse || collapse.classList.contains('show') === expanded) return
40
+
41
+ collapse.classList.toggle('show', expanded)
42
+ const toggle = item.querySelector('[data-bs-toggle="collapse"]')
43
+ if (toggle) {
44
+ toggle.classList.toggle('collapsed', !expanded)
45
+ toggle.setAttribute('aria-expanded', expanded.toString())
46
+ }
9
47
  }
10
48
 
11
49
  add () {
@@ -18,6 +56,8 @@ export default class extends Controller {
18
56
  .replaceAll('NEW_INDEX', this.itemTargets.length.toString())
19
57
 
20
58
  this.itemsTarget.insertAdjacentHTML('beforeend', html)
59
+ // The new item is rendered expanded: enforce single-open accordion behavior.
60
+ this.collapseOthers(this.itemTargets[this.itemTargets.length - 1])
21
61
  this.refresh()
22
62
  }
23
63
 
@@ -45,6 +45,7 @@ export default class extends Controller {
45
45
  this.currentContainerTarget.classList.add('lato-cms-file-field__item--removing')
46
46
  this.removedNoticeTarget.classList.remove('d-none')
47
47
  this.ensureRemoveInput()
48
+ this.syncRequired()
48
49
  }
49
50
 
50
51
  undo() {
@@ -54,6 +55,16 @@ export default class extends Controller {
54
55
  this.removedNoticeTarget.classList.add('d-none')
55
56
  this.removeInput?.remove()
56
57
  this.removeInput = null
58
+ this.syncRequired()
59
+ }
60
+
61
+ // The file input only has to be required while no attachment survives the next
62
+ // save: a persisted attachment (not marked for removal) already satisfies the
63
+ // field, so keeping `required` on the emptied input would block every re-save.
64
+ syncRequired() {
65
+ const hasActiveAttachment = this.hasCurrentContainerTarget &&
66
+ !this.currentContainerTarget.classList.contains('lato-cms-file-field__item--removing')
67
+ this.inputTarget.required = this.fieldRequired && !hasActiveAttachment
57
68
  }
58
69
 
59
70
  ensureRemoveInput() {
@@ -83,6 +94,7 @@ export default class extends Controller {
83
94
  if (attachment) {
84
95
  this.element.insertBefore(this.buildCurrentContainer(attachment), this.newPreviewContainerTarget)
85
96
  }
97
+ this.syncRequired()
86
98
  }
87
99
 
88
100
  findSavedField(fields) {
@@ -150,6 +162,10 @@ export default class extends Controller {
150
162
  return this.element.dataset.fieldId
151
163
  }
152
164
 
165
+ get fieldRequired() {
166
+ return this.element.dataset.fieldRequired === 'true'
167
+ }
168
+
153
169
  get removeLabel() {
154
170
  return this.element.dataset.removeLabel || 'Remove'
155
171
  }
@@ -400,6 +400,19 @@ body.controller-pages.action-show {
400
400
  top: 4px;
401
401
  }
402
402
 
403
+ // ── Repeater items ─────────────────────────────────────────────────────────────
404
+
405
+ .lato-cms-repeater-item__toggle {
406
+ .bi-chevron-down {
407
+ display: inline-block;
408
+ transition: transform 0.2s ease;
409
+ }
410
+
411
+ &.collapsed .bi-chevron-down {
412
+ transform: rotate(-90deg);
413
+ }
414
+ }
415
+
403
416
  .controller-pages-show {
404
417
  .accordion-button {
405
418
  font-weight: 500;
@@ -9,6 +9,10 @@ module LatoCms
9
9
  translations link_translation_action unlink_translation_action
10
10
  ].freeze
11
11
 
12
+ # Field types whose value lives in Active Storage attachments instead of
13
+ # `value`, so `required` must be checked against the attached files.
14
+ ATTACHMENT_FIELD_TYPES = %w[file image video gallery].freeze
15
+
12
16
  before_action { active_sidebar(:lato_cms_pages) }
13
17
  before_action :authenticate_lato_cms_admin, only: ADMIN_ONLY_ACTIONS
14
18
 
@@ -288,6 +292,15 @@ module LatoCms
288
292
 
289
293
  unless field.save
290
294
  errors << { field_id: persisted_field_id, errors: field.errors.full_messages }
295
+ return
296
+ end
297
+
298
+ # Attachments are attached/purged here in the controller, so the model
299
+ # cannot validate `required` for attachment fields. Backstop the
300
+ # client-side enforcement: a required field must end the save with at
301
+ # least one file, otherwise required would be bypassable.
302
+ if ATTACHMENT_FIELD_TYPES.include?(field_type) && field_config&.dig("required") == true && field.files.reload.empty?
303
+ errors << { field_id: persisted_field_id, errors: [t("lato_cms.field_required_attachment_error")] }
291
304
  end
292
305
  end
293
306
 
@@ -318,17 +331,20 @@ module LatoCms
318
331
  end
319
332
  end
320
333
 
321
- def attach_field_files(field, field_data)
322
- return unless field_data[:files].present?
334
+ # Browsers submit empty entries for file inputs with no selection: strip
335
+ # them so `attach` never receives a blank value and the required check
336
+ # sees the field as actually empty.
337
+ def submitted_files(field_data)
338
+ Array(field_data[:files]).reject(&:blank?)
339
+ end
323
340
 
324
- Array(field_data[:files]).compact.each { |file| field.files.attach(file) }
341
+ def attach_field_files(field, field_data)
342
+ submitted_files(field_data).each { |file| field.files.attach(file) }
325
343
  end
326
344
 
327
345
  def assign_file_field_files(field, field_data)
328
- if field.field_settings['multiple'] != true && field_data[:files].present?
329
- new_file = Array(field_data[:files]).compact.first
330
- return unless new_file
331
-
346
+ new_file = submitted_files(field_data).first
347
+ if field.field_settings['multiple'] != true && new_file
332
348
  field.files.each(&:purge)
333
349
  field.files.attach(new_file)
334
350
  else
@@ -346,10 +362,8 @@ module LatoCms
346
362
  end
347
363
 
348
364
  def replace_field_image(field, field_data)
349
- if field_data[:files].present?
350
- new_file = Array(field_data[:files]).compact.first
351
- return unless new_file
352
-
365
+ new_file = submitted_files(field_data).first
366
+ if new_file
353
367
  field.files.each(&:purge)
354
368
  field.files.attach(new_file)
355
369
  else
@@ -361,10 +375,8 @@ module LatoCms
361
375
  # video and its auto-generated poster; removing the video drops the poster
362
376
  # too. Poster generation runs in a background job to keep the save fast.
363
377
  def replace_field_video(field, field_data)
364
- if field_data[:files].present?
365
- new_file = Array(field_data[:files]).compact.first
366
- return unless new_file
367
-
378
+ new_file = submitted_files(field_data).first
379
+ if new_file
368
380
  field.files.each(&:purge)
369
381
  field.files.attach(new_file)
370
382
  LatoCms::GenerateVideoPosterJob.perform_later(field.id)
@@ -7,18 +7,25 @@
7
7
  <%# Seed `min` empty items when the list is empty, so the minimum is met up front. %>
8
8
  <% item_ids = Array.new(min) { SecureRandom.uuid } if item_ids.blank? && min.positive? %>
9
9
 
10
+ <%# Accordion behavior (one item open at a time) hangs off the items container id. %>
11
+ <% items_container_id = "repeater-items-#{tc[:template_component_id]}" %>
12
+ <%# Expand only the first empty item: saved items start collapsed. %>
13
+ <% first_empty_id = item_ids.find { |id| fields_by_item[id].blank? } %>
14
+
10
15
  <div data-controller="lato-cms-repeater"
11
16
  data-lato-cms-repeater-min-value="<%= min %>"
12
17
  data-lato-cms-repeater-max-value="<%= max %>">
13
- <div data-lato-cms-repeater-target="items">
18
+ <div id="<%= items_container_id %>" data-lato-cms-repeater-target="items">
14
19
  <% item_ids.each_with_index do |item_id, item_index| %>
15
20
  <% item_fields = (fields_by_item[item_id] || []).index_by(&:base_field_id) %>
16
- <%= render 'lato_cms/pages/repeater_item', tc: tc, item_id: item_id, item_index: item_index, item_fields: item_fields %>
21
+ <%= render 'lato_cms/pages/repeater_item', tc: tc, item_id: item_id, item_index: item_index, item_fields: item_fields,
22
+ parent_id: items_container_id, expanded: item_id == first_empty_id %>
17
23
  <% end %>
18
24
  </div>
19
25
 
20
26
  <template data-lato-cms-repeater-target="template">
21
- <%= render 'lato_cms/pages/repeater_item', tc: tc, item_id: 'NEW_RECORD', item_index: 'NEW_INDEX', item_fields: {} %>
27
+ <%= render 'lato_cms/pages/repeater_item', tc: tc, item_id: 'NEW_RECORD', item_index: 'NEW_INDEX', item_fields: {},
28
+ parent_id: items_container_id, expanded: true %>
22
29
  </template>
23
30
 
24
31
  <button type="button" class="btn btn-sm btn-outline-primary" data-action="lato-cms-repeater#add" data-lato-cms-repeater-target="addButton">
@@ -1,28 +1,36 @@
1
+ <% collapse_id = "repeater-collapse-#{item_id}" %>
2
+
1
3
  <div class="card mb-3" data-lato-cms-repeater-target="item" data-repeater-item-id="<%= item_id %>">
2
4
  <input type="hidden" name="repeater_order[]" value="<%= item_id %>" data-lato-cms-repeater-target="orderInput">
3
5
 
4
- <div class="card-header d-flex justify-content-between align-items-center py-2">
5
- <strong class="small"><%= tc[:name] %> #<span data-lato-cms-repeater-target="position"><%= item_index.to_i + 1 %></span></strong>
6
- <button type="button" class="btn btn-sm btn-outline-danger" data-action="lato-cms-repeater#remove">
6
+ <div class="card-header d-flex justify-content-between align-items-center gap-2 py-2">
7
+ <button type="button" class="lato-cms-repeater-item__toggle btn btn-link p-0 flex-grow-1 text-start text-decoration-none text-body <%= 'collapsed' unless expanded %>"
8
+ data-bs-toggle="collapse" data-bs-target="#<%= collapse_id %>" aria-expanded="<%= expanded %>" aria-controls="<%= collapse_id %>">
9
+ <i class="bi bi-chevron-down me-2 small"></i>
10
+ <strong class="small"><%= tc[:name] %> #<span data-lato-cms-repeater-target="position"><%= item_index.to_i + 1 %></span></strong>
11
+ </button>
12
+ <button type="button" class="btn btn-sm btn-outline-danger flex-shrink-0" data-action="lato-cms-repeater#remove">
7
13
  <i class="bi bi-trash me-1"></i><%= t('lato_cms.repeater_remove_item') %>
8
14
  </button>
9
15
  </div>
10
16
 
11
- <div class="card-body pb-2">
12
- <% tc[:fields].each do |field_id, field_config| %>
13
- <% page_field = item_fields[field_id.to_s] %>
14
- <% input_name_prefix = "repeater_items[#{item_id}][#{field_id}]" %>
15
- <% dom_id_prefix = "repeater_items_#{item_id}_#{field_id}" %>
17
+ <div id="<%= collapse_id %>" class="collapse <%= 'show' if expanded %>" data-bs-parent="#<%= parent_id %>">
18
+ <div class="card-body pb-2">
19
+ <% tc[:fields].each do |field_id, field_config| %>
20
+ <% page_field = item_fields[field_id.to_s] %>
21
+ <% input_name_prefix = "repeater_items[#{item_id}][#{field_id}]" %>
22
+ <% dom_id_prefix = "repeater_items_#{item_id}_#{field_id}" %>
16
23
 
17
- <div class="mb-3">
18
- <%= lato_cms_render_field(
19
- field_id: field_id,
20
- field_config: field_config,
21
- page_field: page_field,
22
- input_name_prefix: input_name_prefix,
23
- dom_id_prefix: dom_id_prefix
24
- ) %>
25
- </div>
26
- <% end %>
24
+ <div class="mb-3">
25
+ <%= lato_cms_render_field(
26
+ field_id: field_id,
27
+ field_config: field_config,
28
+ page_field: page_field,
29
+ input_name_prefix: input_name_prefix,
30
+ dom_id_prefix: dom_id_prefix
31
+ ) %>
32
+ </div>
33
+ <% end %>
34
+ </div>
27
35
  </div>
28
36
  </div>
@@ -13,6 +13,7 @@
13
13
 
14
14
  <div data-controller="lato-cms-file-field"
15
15
  data-field-id="<%= field_id %>"
16
+ data-field-required="<%= required %>"
16
17
  data-lato-cms-file-field-multiple-value="<%= multiple %>"
17
18
  data-new-badge-label="<%= t('lato_cms.field_file_new_badge') %>"
18
19
  data-remove-label="<%= t('lato_cms.field_file_remove') %>"
@@ -49,6 +50,7 @@
49
50
  id="<%= input_id %>"
50
51
  data-lato-cms-file-field-target="input"
51
52
  data-action="change->lato-cms-file-field#addFiles"
53
+ <%= 'required' if required && !page_field&.files&.attached? %>
52
54
  <%= 'multiple' if multiple %>
53
55
  <% if accept %>accept="<%= accept %>"<% end %>>
54
56
  </div>
@@ -20,7 +20,7 @@ end %>
20
20
  <%= label %><%= ' *' if required %>
21
21
  </label>
22
22
 
23
- <div data-controller="lato-cms-gallery-field" data-lato-cms-gallery-field-field-id-value="<%= field_id %>" data-lato-cms-gallery-field-input-name-prefix-value="<%= local_assigns[:input_name_prefix].presence || "fields[#{field_id}]" %>" data-remove-label="<%= t('lato_cms.field_file_remove') %>">
23
+ <div data-controller="lato-cms-gallery-field" data-lato-cms-gallery-field-field-id-value="<%= field_id %>" data-lato-cms-gallery-field-input-name-prefix-value="<%= local_assigns[:input_name_prefix].presence || "fields[#{field_id}]" %>" data-field-required="<%= required %>" data-remove-label="<%= t('lato_cms.field_file_remove') %>">
24
24
  <div class="lato-cms-gallery-field__grid mb-2"
25
25
  data-lato-cms-gallery-field-target="grid"
26
26
  data-action="dragover->lato-cms-gallery-field#onGridDragOver drop->lato-cms-gallery-field#onGridDrop">
@@ -44,13 +44,17 @@ end %>
44
44
 
45
45
  <label class="btn btn-sm btn-outline-secondary">
46
46
  <i class="bi bi-plus-lg me-1"></i><%= t('lato_cms.field_gallery_add_images') %>
47
+ <%# visually-hidden (not d-none): a required display:none input makes native
48
+ validation fail silently with a "not focusable" error, while a clipped
49
+ 1px input stays focusable so the browser can anchor its message here. %>
47
50
  <input type="file"
48
- class="d-none"
51
+ class="visually-hidden"
49
52
  name="<%= files_input_name %>"
50
53
  accept="<%= accept %>"
51
54
  multiple
52
55
  data-lato-cms-gallery-field-target="fileInput"
53
- data-action="change->lato-cms-gallery-field#addFiles">
56
+ data-action="change->lato-cms-gallery-field#addFiles"
57
+ <%= 'required' if required && ordered_files.empty? %>>
54
58
  </label>
55
59
  <small class="text-muted ms-2"><%= t('lato_cms.field_gallery_drag_to_reorder') %></small>
56
60
  </div>
@@ -13,6 +13,7 @@
13
13
 
14
14
  <div data-controller="lato-cms-image-field"
15
15
  data-field-id="<%= field_id %>"
16
+ data-field-required="<%= required %>"
16
17
  data-new-badge-label="<%= t('lato_cms.field_image_new_badge') %>"
17
18
  data-remove-label="<%= t('lato_cms.field_image_remove') %>"
18
19
  data-remove-pending-label="<%= t('lato_cms.field_image_remove_pending') %>"
@@ -14,6 +14,7 @@
14
14
 
15
15
  <div data-controller="lato-cms-video-field"
16
16
  data-field-id="<%= field_id %>"
17
+ data-field-required="<%= required %>"
17
18
  data-new-badge-label="<%= t('lato_cms.field_video_new_badge') %>"
18
19
  data-remove-label="<%= t('lato_cms.field_video_remove') %>"
19
20
  data-remove-pending-label="<%= t('lato_cms.field_video_remove_pending') %>"
@@ -82,6 +82,7 @@ en:
82
82
  repeater_remove_item: Remove
83
83
  repeater_min_error: "At least %{count} items are required"
84
84
  repeater_max_error: "At most %{count} items are allowed"
85
+ field_required_attachment_error: "At least one file is required"
85
86
  fields_component_missing_alert_html: "Component <strong>%{component_id}</strong> not found. The component configuration may have been removed."
86
87
  fields_previously_saved_data: "Previously saved data:"
87
88
  field_file_remove: Remove
@@ -82,6 +82,7 @@ it:
82
82
  repeater_remove_item: Rimuovi
83
83
  repeater_min_error: "Sono richiesti almeno %{count} elementi"
84
84
  repeater_max_error: "Sono ammessi al massimo %{count} elementi"
85
+ field_required_attachment_error: "È richiesto almeno un file"
85
86
  fields_component_missing_alert_html: "Componente <strong>%{component_id}</strong> non trovato. La configurazione del componente potrebbe essere stata rimossa."
86
87
  fields_previously_saved_data: "Dati salvati in precedenza:"
87
88
  field_file_remove: Rimuovi
@@ -1,3 +1,3 @@
1
1
  module LatoCms
2
- VERSION = "3.0.11"
2
+ VERSION = "3.0.13"
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.0.11
4
+ version: 3.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregorio Galante