lato_cms 3.0.11 → 3.0.12
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/javascripts/lato_cms/controllers/lato_cms_file_field_controller.js +22 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_gallery_field_controller.js +18 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_image_field_controller.js +16 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_video_field_controller.js +16 -0
- data/app/controllers/lato_cms/pages_controller.rb +27 -15
- data/app/views/lato_cms/pages/fields/_file.html.erb +2 -0
- data/app/views/lato_cms/pages/fields/_gallery.html.erb +7 -3
- data/app/views/lato_cms/pages/fields/_image.html.erb +1 -0
- data/app/views/lato_cms/pages/fields/_video.html.erb +1 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/it.yml +1 -0
- 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: 6fd8416ab840a11e364752d13f21440d8282862df78e4b4b67ef8f69432aa56b
|
|
4
|
+
data.tar.gz: 544411ddd79120e75f9f3df5cf7136d9d0d940cd3ba15abb0720fbf95ec368f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0d6f6b71abf13818513d8921e6daa52254ea32209d9b0a44ff3b25f3d3d1e83e4dc90e262eeefba8f9ac08a7fe5bf65d518f6a3c638dfabcdfb072b238ba28d
|
|
7
|
+
data.tar.gz: f899bc4fbe56254e67359839b79a29598ba0507d05704d7e8cb8cded33f0675f3499bb6aac33666b0b4f2b5eae8f18614e3b3b0f2f0c68fb00c4b22f136af7ba
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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)
|
|
@@ -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="
|
|
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') %>"
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
data/config/locales/it.yml
CHANGED
|
@@ -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
|
data/lib/lato_cms/version.rb
CHANGED