lato_cms 3.0.12 → 3.1.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/app/assets/javascripts/lato_cms/controllers/lato_cms_media_field_controller.js +143 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_media_picker_controller.js +110 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_media_reorder_controller.js +37 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_reload_on_modal_close_controller.js +37 -0
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_repeater_controller.js +40 -0
- data/app/assets/stylesheets/lato_cms/application.scss +141 -155
- data/app/controllers/lato_cms/application_controller.rb +4 -0
- data/app/controllers/lato_cms/media_controller.rb +121 -0
- data/app/controllers/lato_cms/pages_controller.rb +75 -91
- data/app/helpers/lato_cms/media_helper.rb +50 -0
- data/app/jobs/lato_cms/export_sitemap_csv_job.rb +33 -0
- data/app/jobs/lato_cms/generate_alt_text_job.rb +21 -0
- data/app/jobs/lato_cms/generate_video_poster_job.rb +7 -5
- data/app/jobs/lato_cms/translate_component_fields_job.rb +16 -0
- data/app/models/lato_cms/llm_client.rb +31 -0
- data/app/models/lato_cms/media.rb +270 -0
- data/app/models/lato_cms/page.rb +40 -0
- data/app/models/lato_cms/page_field.rb +52 -80
- data/app/models/lato_cms/page_field_media.rb +8 -0
- data/app/views/lato_cms/media/_form_create.html.erb +20 -0
- data/app/views/lato_cms/media/_form_update.html.erb +60 -0
- data/app/views/lato_cms/media/create.html.erb +13 -0
- data/app/views/lato_cms/media/index.html.erb +22 -0
- data/app/views/lato_cms/media/picker_action.html.erb +98 -0
- data/app/views/lato_cms/media/update.html.erb +13 -0
- data/app/views/lato_cms/pages/_component_accordion.html.erb +15 -0
- data/app/views/lato_cms/pages/_repeater.html.erb +10 -3
- data/app/views/lato_cms/pages/_repeater_item.html.erb +26 -18
- data/app/views/lato_cms/pages/fields/_file.html.erb +4 -55
- data/app/views/lato_cms/pages/fields/_gallery.html.erb +4 -60
- data/app/views/lato_cms/pages/fields/_image.html.erb +4 -68
- data/app/views/lato_cms/pages/fields/_media_field.html.erb +62 -0
- data/app/views/lato_cms/pages/fields/_media_item.html.erb +27 -0
- data/app/views/lato_cms/pages/fields/_video.html.erb +4 -69
- data/app/views/lato_cms/pages/index.html.erb +17 -0
- data/app/views/lato_cms/pages/show.html.erb +2 -1
- data/config/locales/en.yml +36 -12
- data/config/locales/it.yml +36 -12
- data/config/routes.rb +12 -0
- data/db/migrate/20260728175413_create_lato_cms_media.rb +11 -0
- data/db/migrate/20260728175414_create_lato_cms_page_field_media.rb +12 -0
- data/db/migrate/20260729120000_change_lato_cms_media_alt_text_to_translations.rb +25 -0
- data/lib/lato_cms/config.rb +12 -1
- data/lib/lato_cms/version.rb +1 -1
- metadata +38 -5
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_file_field_controller.js +0 -225
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_gallery_field_controller.js +0 -190
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_image_field_controller.js +0 -169
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_video_field_controller.js +0 -180
|
@@ -1,56 +1,5 @@
|
|
|
1
|
-
<% label = field_config['name'] || field_id.humanize %>
|
|
2
|
-
<% required = field_config['required'] == true %>
|
|
3
1
|
<% settings = field_config['settings'] || {} %>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<% input_id = lato_cms_field_dom_id(field_id, 'files', dom_id_prefix: local_assigns[:dom_id_prefix]) %>
|
|
9
|
-
|
|
10
|
-
<label class="form-label">
|
|
11
|
-
<%= label %><%= ' *' if required %>
|
|
12
|
-
</label>
|
|
13
|
-
|
|
14
|
-
<div data-controller="lato-cms-file-field"
|
|
15
|
-
data-field-id="<%= field_id %>"
|
|
16
|
-
data-field-required="<%= required %>"
|
|
17
|
-
data-lato-cms-file-field-multiple-value="<%= multiple %>"
|
|
18
|
-
data-new-badge-label="<%= t('lato_cms.field_file_new_badge') %>"
|
|
19
|
-
data-remove-label="<%= t('lato_cms.field_file_remove') %>"
|
|
20
|
-
data-remove-pending-label="<%= t('lato_cms.field_file_remove_pending') %>"
|
|
21
|
-
data-undo-label="<%= t('lato_cms.field_file_remove_undo') %>">
|
|
22
|
-
<div class="mb-2" data-lato-cms-file-field-target="list">
|
|
23
|
-
<% if page_field&.files&.attached? %>
|
|
24
|
-
<% page_field.files.each do |file| %>
|
|
25
|
-
<div class="lato-cms-file-field__item" data-lato-cms-file-field-target="item" data-attachment-id="<%= file.id %>">
|
|
26
|
-
<div class="lato-cms-file-field__info">
|
|
27
|
-
<i class="bi bi-paperclip"></i>
|
|
28
|
-
<span><%= file.blob.filename %></span>
|
|
29
|
-
<span class="text-muted small">(<%= number_to_human_size(file.blob.byte_size) %>)</span>
|
|
30
|
-
</div>
|
|
31
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_file_remove') %>" aria-label="<%= t('lato_cms.field_file_remove') %>" data-action="lato-cms-file-field#remove">
|
|
32
|
-
<i class="bi bi-trash"></i>
|
|
33
|
-
</button>
|
|
34
|
-
<div class="lato-cms-file-field__removed d-none" data-lato-cms-file-field-target="removedNotice">
|
|
35
|
-
<span class="text-danger small">
|
|
36
|
-
<i class="bi bi-trash me-1"></i><%= t('lato_cms.field_file_remove_pending') %>
|
|
37
|
-
</span>
|
|
38
|
-
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-file-field#undo">
|
|
39
|
-
<%= t('lato_cms.field_file_remove_undo') %>
|
|
40
|
-
</button>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
<% end %>
|
|
44
|
-
<% end %>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<input type="file"
|
|
48
|
-
class="form-control"
|
|
49
|
-
name="<%= files_input_name %>"
|
|
50
|
-
id="<%= input_id %>"
|
|
51
|
-
data-lato-cms-file-field-target="input"
|
|
52
|
-
data-action="change->lato-cms-file-field#addFiles"
|
|
53
|
-
<%= 'required' if required && !page_field&.files&.attached? %>
|
|
54
|
-
<%= 'multiple' if multiple %>
|
|
55
|
-
<% if accept %>accept="<%= accept %>"<% end %>>
|
|
56
|
-
</div>
|
|
2
|
+
<%= render partial: 'lato_cms/pages/fields/media_field', locals: {
|
|
3
|
+
field_id: field_id, field_config: field_config, page_field: page_field, kind: 'file', multiple: settings['multiple'] == true,
|
|
4
|
+
input_name_prefix: local_assigns[:input_name_prefix], dom_id_prefix: local_assigns[:dom_id_prefix]
|
|
5
|
+
} %>
|
|
@@ -1,60 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<% files_input_name = lato_cms_field_input_name(field_id, 'files', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
|
|
6
|
-
|
|
7
|
-
<%# Build ordered file list from value JSON or default order %>
|
|
8
|
-
<% ordered_files = begin
|
|
9
|
-
if page_field&.files&.attached?
|
|
10
|
-
order = JSON.parse(page_field.value || '[]') rescue []
|
|
11
|
-
files = page_field.files.to_a
|
|
12
|
-
order.filter_map { |id| files.find { |f| f.id.to_s == id.to_s } } +
|
|
13
|
-
files.reject { |f| order.include?(f.id.to_s) }
|
|
14
|
-
else
|
|
15
|
-
[]
|
|
16
|
-
end
|
|
17
|
-
end %>
|
|
18
|
-
|
|
19
|
-
<label class="form-label">
|
|
20
|
-
<%= label %><%= ' *' if required %>
|
|
21
|
-
</label>
|
|
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-field-required="<%= required %>" data-remove-label="<%= t('lato_cms.field_file_remove') %>">
|
|
24
|
-
<div class="lato-cms-gallery-field__grid mb-2"
|
|
25
|
-
data-lato-cms-gallery-field-target="grid"
|
|
26
|
-
data-action="dragover->lato-cms-gallery-field#onGridDragOver drop->lato-cms-gallery-field#onGridDrop">
|
|
27
|
-
<% ordered_files.each do |file| %>
|
|
28
|
-
<div class="lato-cms-gallery-field__item"
|
|
29
|
-
data-gallery-item
|
|
30
|
-
data-attachment-id="<%= file.id %>"
|
|
31
|
-
draggable="true"
|
|
32
|
-
data-action="dragstart->lato-cms-gallery-field#onDragStart dragend->lato-cms-gallery-field#onDragEnd">
|
|
33
|
-
<img src="<%= lato_cms_attachment_path(file) %>" class="lato-cms-gallery-field__thumb" alt="<%= file.filename %>">
|
|
34
|
-
<button type="button" class="lato-cms-attachment-field__remove lato-cms-gallery-field__remove" title="<%= t('lato_cms.field_file_remove') %>" aria-label="<%= t('lato_cms.field_file_remove') %>" data-action="click->lato-cms-gallery-field#remove">
|
|
35
|
-
<i class="bi bi-trash"></i>
|
|
36
|
-
</button>
|
|
37
|
-
</div>
|
|
38
|
-
<% end %>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<% if ordered_files.empty? %>
|
|
42
|
-
<p class="text-muted small mb-2" data-lato-cms-gallery-field-target="emptyMsg"><%= t('lato_cms.field_gallery_empty') %></p>
|
|
43
|
-
<% end %>
|
|
44
|
-
|
|
45
|
-
<label class="btn btn-sm btn-outline-secondary">
|
|
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. %>
|
|
50
|
-
<input type="file"
|
|
51
|
-
class="visually-hidden"
|
|
52
|
-
name="<%= files_input_name %>"
|
|
53
|
-
accept="<%= accept %>"
|
|
54
|
-
multiple
|
|
55
|
-
data-lato-cms-gallery-field-target="fileInput"
|
|
56
|
-
data-action="change->lato-cms-gallery-field#addFiles"
|
|
57
|
-
<%= 'required' if required && ordered_files.empty? %>>
|
|
58
|
-
</label>
|
|
59
|
-
<small class="text-muted ms-2"><%= t('lato_cms.field_gallery_drag_to_reorder') %></small>
|
|
60
|
-
</div>
|
|
1
|
+
<%= render partial: 'lato_cms/pages/fields/media_field', locals: {
|
|
2
|
+
field_id: field_id, field_config: field_config, page_field: page_field, kind: 'image', multiple: true,
|
|
3
|
+
input_name_prefix: local_assigns[:input_name_prefix], dom_id_prefix: local_assigns[:dom_id_prefix]
|
|
4
|
+
} %>
|
|
@@ -1,68 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<% current_file = page_field&.files&.attached? ? page_field.files.first : nil %>
|
|
6
|
-
<% files_input_name = lato_cms_field_input_name(field_id, 'files', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
|
|
7
|
-
<% remove_input_name = lato_cms_field_input_name(field_id, 'remove_file_ids', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
|
|
8
|
-
<% input_id = lato_cms_field_dom_id(field_id, 'files', dom_id_prefix: local_assigns[:dom_id_prefix]) %>
|
|
9
|
-
|
|
10
|
-
<label class="form-label">
|
|
11
|
-
<%= label %><%= ' *' if required %>
|
|
12
|
-
</label>
|
|
13
|
-
|
|
14
|
-
<div data-controller="lato-cms-image-field"
|
|
15
|
-
data-field-id="<%= field_id %>"
|
|
16
|
-
data-field-required="<%= required %>"
|
|
17
|
-
data-new-badge-label="<%= t('lato_cms.field_image_new_badge') %>"
|
|
18
|
-
data-remove-label="<%= t('lato_cms.field_image_remove') %>"
|
|
19
|
-
data-remove-pending-label="<%= t('lato_cms.field_image_remove_pending') %>"
|
|
20
|
-
data-undo-label="<%= t('lato_cms.field_file_remove_undo') %>">
|
|
21
|
-
<% if current_file %>
|
|
22
|
-
<div class="lato-cms-file-field__item lato-cms-image-field__item" data-lato-cms-image-field-target="currentContainer" data-attachment-id="<%= current_file.id %>">
|
|
23
|
-
<div class="lato-cms-image-field__preview">
|
|
24
|
-
<img src="<%= lato_cms_attachment_path(current_file) %>" class="lato-cms-image-field__thumb" data-lato-cms-image-field-target="currentImage" alt="<%= current_file.filename %>">
|
|
25
|
-
</div>
|
|
26
|
-
<div class="lato-cms-file-field__info">
|
|
27
|
-
<i class="bi bi-image"></i>
|
|
28
|
-
<span><%= current_file.blob.filename %></span>
|
|
29
|
-
<span class="text-muted small">(<%= number_to_human_size(current_file.blob.byte_size) %>)</span>
|
|
30
|
-
</div>
|
|
31
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_image_remove') %>" aria-label="<%= t('lato_cms.field_image_remove') %>" data-action="lato-cms-image-field#remove">
|
|
32
|
-
<i class="bi bi-trash"></i>
|
|
33
|
-
</button>
|
|
34
|
-
<div class="lato-cms-file-field__removed d-none" data-lato-cms-image-field-target="removedNotice">
|
|
35
|
-
<span class="text-danger small">
|
|
36
|
-
<i class="bi bi-trash me-1"></i><%= t('lato_cms.field_image_remove_pending') %>
|
|
37
|
-
</span>
|
|
38
|
-
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-image-field#undo">
|
|
39
|
-
<%= t('lato_cms.field_file_remove_undo') %>
|
|
40
|
-
</button>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
<% end %>
|
|
44
|
-
|
|
45
|
-
<div class="lato-cms-file-field__item lato-cms-file-field__item--new lato-cms-image-field__item d-none" data-lato-cms-image-field-target="newPreviewContainer">
|
|
46
|
-
<div class="lato-cms-image-field__preview">
|
|
47
|
-
<img src="" class="lato-cms-image-field__thumb" data-lato-cms-image-field-target="newPreview" alt="">
|
|
48
|
-
</div>
|
|
49
|
-
<div class="lato-cms-file-field__info">
|
|
50
|
-
<i class="bi bi-image"></i>
|
|
51
|
-
<span data-lato-cms-image-field-target="newPreviewName"></span>
|
|
52
|
-
<span class="text-muted small" data-lato-cms-image-field-target="newPreviewSize"></span>
|
|
53
|
-
<span class="badge bg-success"><%= t('lato_cms.field_image_new_badge') %></span>
|
|
54
|
-
</div>
|
|
55
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_image_remove') %>" aria-label="<%= t('lato_cms.field_image_remove') %>" data-action="lato-cms-image-field#removeNew">
|
|
56
|
-
<i class="bi bi-trash"></i>
|
|
57
|
-
</button>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<input type="file"
|
|
61
|
-
class="form-control"
|
|
62
|
-
name="<%= files_input_name %>"
|
|
63
|
-
id="<%= input_id %>"
|
|
64
|
-
accept="<%= accept %>"
|
|
65
|
-
data-lato-cms-image-field-target="input"
|
|
66
|
-
data-action="change->lato-cms-image-field#preview"
|
|
67
|
-
<%= 'required' if required && !current_file %>>
|
|
68
|
-
</div>
|
|
1
|
+
<%= render partial: 'lato_cms/pages/fields/media_field', locals: {
|
|
2
|
+
field_id: field_id, field_config: field_config, page_field: page_field, kind: 'image', multiple: false,
|
|
3
|
+
input_name_prefix: local_assigns[:input_name_prefix], dom_id_prefix: local_assigns[:dom_id_prefix]
|
|
4
|
+
} %>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<%# Shared markup for image/video/file/gallery fields. Rendered by the 4
|
|
2
|
+
type-specific partials with `kind` ('image'/'video'/'file') and `multiple`
|
|
3
|
+
locals — differences between them are otherwise just presentation. %>
|
|
4
|
+
<% label = field_config['name'] || field_id.humanize %>
|
|
5
|
+
<% required = field_config['required'] == true %>
|
|
6
|
+
<% current_media = page_field ? page_field.media.to_a : [] %>
|
|
7
|
+
<% dom_id = lato_cms_field_dom_id(field_id, 'media_picker', dom_id_prefix: local_assigns[:dom_id_prefix]) %>
|
|
8
|
+
<% frame_id = "media_picker_#{dom_id}" %>
|
|
9
|
+
<% hidden_name = multiple ?
|
|
10
|
+
lato_cms_field_input_name(field_id, 'media_ids', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) :
|
|
11
|
+
lato_cms_field_input_name(field_id, 'media_id', input_name_prefix: local_assigns[:input_name_prefix]) %>
|
|
12
|
+
<% picker_type = kind == 'file' ? nil : kind %>
|
|
13
|
+
|
|
14
|
+
<label class="form-label">
|
|
15
|
+
<%= label %><%= ' *' if required %>
|
|
16
|
+
</label>
|
|
17
|
+
|
|
18
|
+
<div data-controller="lato-cms-media-field<%= ' lato-cms-media-reorder' if multiple %>"
|
|
19
|
+
data-lato-cms-media-field-kind-value="<%= kind %>"
|
|
20
|
+
data-lato-cms-media-field-multiple-value="<%= multiple %>"
|
|
21
|
+
data-lato-cms-media-field-frame-id-value="<%= frame_id %>"
|
|
22
|
+
data-lato-cms-media-field-hidden-name-value="<%= hidden_name %>"
|
|
23
|
+
data-lato-cms-media-field-field-id-value="<%= field_id %>"
|
|
24
|
+
data-field-required="<%= required %>">
|
|
25
|
+
|
|
26
|
+
<div class="lato-cms-media-field__grid mb-2"
|
|
27
|
+
data-lato-cms-media-field-target="grid"
|
|
28
|
+
data-lato-cms-media-reorder-target="grid"
|
|
29
|
+
<% if multiple %>data-action="dragover->lato-cms-media-reorder#onGridDragOver drop->lato-cms-media-reorder#onGridDrop"<% end %>>
|
|
30
|
+
<% current_media.each do |m| %>
|
|
31
|
+
<%= render partial: 'lato_cms/pages/fields/media_item', locals: { media: m, kind: kind, hidden_name: hidden_name, multiple: multiple } %>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<%# The empty state AND the "add more" affordance for multi-value fields —
|
|
35
|
+
a single dashed tile living inside the grid, hidden (not removed) for
|
|
36
|
+
single-value fields once something is selected, so the field-side JS
|
|
37
|
+
only ever has to toggle a class rather than rebuild this node. %>
|
|
38
|
+
<%# Deliberately NOT using the shared `lato-action` trigger convention here:
|
|
39
|
+
it opens frames by hoping Turbo's own click handling notices a
|
|
40
|
+
just-inserted <turbo-frame> before driving a full-page visit, which
|
|
41
|
+
loses that race for this link. `openPicker` below prevents the
|
|
42
|
+
default navigation itself and sets the frame's `src` explicitly
|
|
43
|
+
(Turbo's documented, non-racy way to trigger a frame-scoped fetch). %>
|
|
44
|
+
<%= link_to lato_cms.media_picker_action_path(frame_id: frame_id, multiple: multiple, type: picker_type),
|
|
45
|
+
class: "lato-cms-media-field__tile lato-cms-media-field__tile--add#{' d-none' if !multiple && current_media.any?}",
|
|
46
|
+
data: {
|
|
47
|
+
lato_cms_media_field_target: 'addTile',
|
|
48
|
+
action: 'click->lato-cms-media-field#openPicker',
|
|
49
|
+
action_title: t('lato_cms.media_picker_title'), action_size: 'lg'
|
|
50
|
+
} do %>
|
|
51
|
+
<i class="bi bi-images"></i>
|
|
52
|
+
<span><%= t('lato_cms.media_choose_from_library') %></span>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<%# Real-but-hidden input anchors the browser's native required-validation
|
|
57
|
+
bubble (a display:none required input fails silently as "not
|
|
58
|
+
focusable"). Kept in sync with selection state by the Stimulus controller. %>
|
|
59
|
+
<input type="text" class="visually-hidden" value="<%= current_media.first&.id %>"
|
|
60
|
+
data-lato-cms-media-field-target="validationAnchor"
|
|
61
|
+
<%= 'required' if required && current_media.empty? %>>
|
|
62
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<div class="lato-cms-media-field__tile lato-cms-media-field__item"
|
|
2
|
+
data-media-id="<%= media.id %>"
|
|
3
|
+
data-lato-cms-media-field-target="item"
|
|
4
|
+
<% if multiple %>
|
|
5
|
+
data-lato-cms-media-reorder-target="item"
|
|
6
|
+
draggable="true"
|
|
7
|
+
data-action="dragstart->lato-cms-media-reorder#onDragStart dragend->lato-cms-media-reorder#onDragEnd"
|
|
8
|
+
<% end %>>
|
|
9
|
+
<% if kind == 'video' %>
|
|
10
|
+
<video muted preload="metadata" class="lato-cms-media-field__player" src="<%= media.url %>"></video>
|
|
11
|
+
<i class="bi bi-play-circle-fill lato-cms-media-field__badge"></i>
|
|
12
|
+
<% elsif media.thumbnail_url %>
|
|
13
|
+
<img src="<%= media.thumbnail_url %>" class="lato-cms-media-field__thumb" alt="<%= media.alt_text %>">
|
|
14
|
+
<% else %>
|
|
15
|
+
<div class="lato-cms-media-field__icon"><i class="bi bi-paperclip"></i></div>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<div class="lato-cms-media-field__overlay">
|
|
19
|
+
<span class="lato-cms-media-field__name"><%= media.name %></span>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<button type="button" class="lato-cms-media-field__remove" title="<%= t('lato_cms.field_file_remove') %>" aria-label="<%= t('lato_cms.field_file_remove') %>" data-action="lato-cms-media-field#remove">
|
|
23
|
+
<i class="bi bi-x-lg"></i>
|
|
24
|
+
</button>
|
|
25
|
+
|
|
26
|
+
<input type="hidden" name="<%= hidden_name %>" value="<%= media.id %>" data-lato-cms-media-field-target="hiddenInput">
|
|
27
|
+
</div>
|
|
@@ -1,69 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<% current_file = page_field&.video_attachment %>
|
|
6
|
-
<% poster_file = page_field&.poster_attachment %>
|
|
7
|
-
<% files_input_name = lato_cms_field_input_name(field_id, 'files', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
|
|
8
|
-
<% remove_input_name = lato_cms_field_input_name(field_id, 'remove_file_ids', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
|
|
9
|
-
<% input_id = lato_cms_field_dom_id(field_id, 'files', dom_id_prefix: local_assigns[:dom_id_prefix]) %>
|
|
10
|
-
|
|
11
|
-
<label class="form-label">
|
|
12
|
-
<%= label %><%= ' *' if required %>
|
|
13
|
-
</label>
|
|
14
|
-
|
|
15
|
-
<div data-controller="lato-cms-video-field"
|
|
16
|
-
data-field-id="<%= field_id %>"
|
|
17
|
-
data-field-required="<%= required %>"
|
|
18
|
-
data-new-badge-label="<%= t('lato_cms.field_video_new_badge') %>"
|
|
19
|
-
data-remove-label="<%= t('lato_cms.field_video_remove') %>"
|
|
20
|
-
data-remove-pending-label="<%= t('lato_cms.field_video_remove_pending') %>"
|
|
21
|
-
data-undo-label="<%= t('lato_cms.field_file_remove_undo') %>">
|
|
22
|
-
<% if current_file %>
|
|
23
|
-
<div class="lato-cms-file-field__item lato-cms-video-field__item" data-lato-cms-video-field-target="currentContainer" data-attachment-id="<%= current_file.id %>">
|
|
24
|
-
<div class="lato-cms-video-field__preview">
|
|
25
|
-
<video controls preload="metadata" class="lato-cms-video-field__player" src="<%= lato_cms_attachment_path(current_file) %>" <% if poster_file %>poster="<%= lato_cms_attachment_path(poster_file) %>"<% end %> data-lato-cms-video-field-target="currentVideo"></video>
|
|
26
|
-
</div>
|
|
27
|
-
<div class="lato-cms-file-field__info">
|
|
28
|
-
<i class="bi bi-film"></i>
|
|
29
|
-
<span><%= current_file.blob.filename %></span>
|
|
30
|
-
<span class="text-muted small">(<%= number_to_human_size(current_file.blob.byte_size) %>)</span>
|
|
31
|
-
</div>
|
|
32
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_video_remove') %>" aria-label="<%= t('lato_cms.field_video_remove') %>" data-action="lato-cms-video-field#remove">
|
|
33
|
-
<i class="bi bi-trash"></i>
|
|
34
|
-
</button>
|
|
35
|
-
<div class="lato-cms-file-field__removed d-none" data-lato-cms-video-field-target="removedNotice">
|
|
36
|
-
<span class="text-danger small">
|
|
37
|
-
<i class="bi bi-trash me-1"></i><%= t('lato_cms.field_video_remove_pending') %>
|
|
38
|
-
</span>
|
|
39
|
-
<button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-video-field#undo">
|
|
40
|
-
<%= t('lato_cms.field_file_remove_undo') %>
|
|
41
|
-
</button>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
<% end %>
|
|
45
|
-
|
|
46
|
-
<div class="lato-cms-file-field__item lato-cms-file-field__item--new lato-cms-video-field__item d-none" data-lato-cms-video-field-target="newPreviewContainer">
|
|
47
|
-
<div class="lato-cms-video-field__preview">
|
|
48
|
-
<video controls preload="metadata" class="lato-cms-video-field__player" data-lato-cms-video-field-target="newPreview"></video>
|
|
49
|
-
</div>
|
|
50
|
-
<div class="lato-cms-file-field__info">
|
|
51
|
-
<i class="bi bi-film"></i>
|
|
52
|
-
<span data-lato-cms-video-field-target="newPreviewName"></span>
|
|
53
|
-
<span class="text-muted small" data-lato-cms-video-field-target="newPreviewSize"></span>
|
|
54
|
-
<span class="badge bg-success"><%= t('lato_cms.field_video_new_badge') %></span>
|
|
55
|
-
</div>
|
|
56
|
-
<button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_video_remove') %>" aria-label="<%= t('lato_cms.field_video_remove') %>" data-action="lato-cms-video-field#removeNew">
|
|
57
|
-
<i class="bi bi-trash"></i>
|
|
58
|
-
</button>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<input type="file"
|
|
62
|
-
class="form-control"
|
|
63
|
-
name="<%= files_input_name %>"
|
|
64
|
-
id="<%= input_id %>"
|
|
65
|
-
accept="<%= accept %>"
|
|
66
|
-
data-lato-cms-video-field-target="input"
|
|
67
|
-
data-action="change->lato-cms-video-field#preview"
|
|
68
|
-
<%= 'required' if required && !current_file %>>
|
|
69
|
-
</div>
|
|
1
|
+
<%= render partial: 'lato_cms/pages/fields/media_field', locals: {
|
|
2
|
+
field_id: field_id, field_config: field_config, page_field: page_field, kind: 'video', multiple: false,
|
|
3
|
+
input_name_prefix: local_assigns[:input_name_prefix], dom_id_prefix: local_assigns[:dom_id_prefix]
|
|
4
|
+
} %>
|
|
@@ -10,6 +10,23 @@
|
|
|
10
10
|
<%= locale_to_flag(locale) %> <%= locale.to_s.upcase %>
|
|
11
11
|
<% end %>
|
|
12
12
|
<% end %>
|
|
13
|
+
|
|
14
|
+
<div class="dropdown ms-auto" title="<%= t('lato_cms.cta_actions') %>" data-controller="lato-tooltip">
|
|
15
|
+
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
|
16
|
+
<i class="bi bi-three-dots"></i>
|
|
17
|
+
</button>
|
|
18
|
+
<ul class="dropdown-menu dropdown-menu-end">
|
|
19
|
+
<li>
|
|
20
|
+
<%= link_to t('lato_cms.sitemap_export_cta'), lato_cms.pages_sitemap_export_action_path, class: 'dropdown-item',
|
|
21
|
+
data: {
|
|
22
|
+
lato_action_target: 'trigger',
|
|
23
|
+
turbo_method: :post,
|
|
24
|
+
turbo_frame: 'lato_operation',
|
|
25
|
+
action_title: t('lato_cms.sitemap_export_title')
|
|
26
|
+
} %>
|
|
27
|
+
</li>
|
|
28
|
+
</ul>
|
|
29
|
+
</div>
|
|
13
30
|
</div>
|
|
14
31
|
<div class="card-body">
|
|
15
32
|
<%= lato_index @pages,
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
</div>
|
|
71
71
|
|
|
72
72
|
<%# ── Editor column ──────────────────────────────────────────────────── %>
|
|
73
|
-
<div class="col-12 col-lg-6 mb-4 lato-cms-advanced-editor__editor-col" data-lato-cms-advanced-editor-target="editorCol"
|
|
73
|
+
<div class="col-12 col-lg-6 mb-4 lato-cms-advanced-editor__editor-col" data-lato-cms-advanced-editor-target="editorCol"
|
|
74
|
+
data-controller="lato-cms-reload-on-modal-close">
|
|
74
75
|
<div class="card h-100">
|
|
75
76
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
76
77
|
<h2 class="fs-5 mb-0"><%= t('lato_cms.show_editor_title') %></h2>
|
data/config/locales/en.yml
CHANGED
|
@@ -33,6 +33,10 @@ en:
|
|
|
33
33
|
form_template_unavailable: "The template \"%{template_id}\" is no longer available. Please select a different template."
|
|
34
34
|
action_view_frontend: View
|
|
35
35
|
cta_actions: Actions
|
|
36
|
+
sitemap_export_cta: Export sitemap (CSV)
|
|
37
|
+
sitemap_export_title: Sitemap CSV export
|
|
38
|
+
sitemap_export_message: "%{count} pages exported"
|
|
39
|
+
sitemap_export_failed: Failed to start sitemap export
|
|
36
40
|
translations_title: Translations
|
|
37
41
|
translation_no_other_locales: No other locales are configured.
|
|
38
42
|
translation_link_cta: Link
|
|
@@ -70,8 +74,12 @@ en:
|
|
|
70
74
|
component_clone_cta: Clone
|
|
71
75
|
component_clone_from: "Clone from %{lang}"
|
|
72
76
|
component_clone_confirm: "Replace this component's fields with the ones from the %{lang} page?"
|
|
77
|
+
component_clone_translate_from: "Clone & translate from %{lang}"
|
|
78
|
+
component_clone_translate_confirm: "Replace this component's fields with translated copies from the %{lang} page?"
|
|
73
79
|
component_cloned: Component cloned successfully
|
|
74
80
|
component_clone_failed: Failed to clone component
|
|
81
|
+
component_clone_translate_failed: Fields were cloned, but failed to start translation
|
|
82
|
+
component_clone_translated: Fields translated successfully
|
|
75
83
|
component_state_saving: Updating...
|
|
76
84
|
component_state_saved: State updated
|
|
77
85
|
component_state_save_error: Failed to update state
|
|
@@ -86,19 +94,35 @@ en:
|
|
|
86
94
|
fields_component_missing_alert_html: "Component <strong>%{component_id}</strong> not found. The component configuration may have been removed."
|
|
87
95
|
fields_previously_saved_data: "Previously saved data:"
|
|
88
96
|
field_file_remove: Remove
|
|
89
|
-
field_file_remove_pending: File will be removed on save
|
|
90
|
-
field_file_remove_undo: Undo
|
|
91
|
-
field_file_new_badge: New
|
|
92
|
-
field_gallery_empty: No images yet. Add images below.
|
|
93
|
-
field_gallery_add_images: Add images
|
|
94
|
-
field_gallery_drag_to_reorder: Drag to reorder
|
|
95
|
-
field_image_remove: Remove
|
|
96
|
-
field_image_remove_pending: Image will be removed on save
|
|
97
|
-
field_image_new_badge: New
|
|
98
|
-
field_video_remove: Remove
|
|
99
|
-
field_video_remove_pending: Video will be removed on save
|
|
100
|
-
field_video_new_badge: New
|
|
101
97
|
field_multiselect_hint: Hold Ctrl/Cmd to select multiple
|
|
98
|
+
media_index_title: Media
|
|
99
|
+
media_create_title: Upload media
|
|
100
|
+
media_create_cta: Upload
|
|
101
|
+
media_update_title: Edit media
|
|
102
|
+
media_alt_text: Alt text
|
|
103
|
+
media_alt_text_regenerate: Regenerate with AI
|
|
104
|
+
media_alt_text_regenerate_confirm: "This will overwrite the current alt text in every language with a new AI-generated one. Continue?"
|
|
105
|
+
media_alt_text_regenerated: Alt text regenerated
|
|
106
|
+
media_alt_text_regenerate_unavailable: Alt text can only be regenerated for images, and only when an LLM is configured
|
|
107
|
+
media_alt_text_regenerate_failed: Failed to start alt text regeneration
|
|
108
|
+
media_created: Media uploaded successfully
|
|
109
|
+
media_updated: Media updated successfully
|
|
110
|
+
media_deleted: Media deleted successfully
|
|
111
|
+
media_delete_in_use: "Cannot delete: used in %{count} place(s)"
|
|
112
|
+
media_delete_failed: Failed to delete media
|
|
113
|
+
media_choose_from_library: Choose from library
|
|
114
|
+
media_picker_title: Media library
|
|
115
|
+
media_picker_tab_browse: Browse library
|
|
116
|
+
media_picker_tab_upload: Upload new
|
|
117
|
+
media_picker_empty: No media found.
|
|
118
|
+
media_picker_confirm: Confirm selection
|
|
119
|
+
media_picker_selected_label: selected
|
|
120
|
+
media_search_placeholder: Search media...
|
|
121
|
+
media_upload_cta: Upload
|
|
122
|
+
media_type_all: All
|
|
123
|
+
media_type_image: Images
|
|
124
|
+
media_type_video: Videos
|
|
125
|
+
media_type_file: Files
|
|
102
126
|
field_select_placeholder: "-- Select --"
|
|
103
127
|
field_text_toolbar_bold: Bold
|
|
104
128
|
field_text_toolbar_italic: Italic
|
data/config/locales/it.yml
CHANGED
|
@@ -33,6 +33,10 @@ it:
|
|
|
33
33
|
form_template_unavailable: "Il template \"%{template_id}\" non e piu disponibile. Seleziona un template diverso."
|
|
34
34
|
action_view_frontend: Anteprima
|
|
35
35
|
cta_actions: Azioni
|
|
36
|
+
sitemap_export_cta: Esporta sitemap (CSV)
|
|
37
|
+
sitemap_export_title: Esportazione sitemap CSV
|
|
38
|
+
sitemap_export_message: "%{count} pagine esportate"
|
|
39
|
+
sitemap_export_failed: Impossibile avviare l'esportazione della sitemap
|
|
36
40
|
translations_title: Traduzioni
|
|
37
41
|
translation_no_other_locales: Nessuna altra lingua configurata.
|
|
38
42
|
translation_link_cta: Collega
|
|
@@ -70,8 +74,12 @@ it:
|
|
|
70
74
|
component_clone_cta: Clona
|
|
71
75
|
component_clone_from: "Clona da %{lang}"
|
|
72
76
|
component_clone_confirm: "Sostituire i campi di questa componente con quelli della pagina in %{lang}?"
|
|
77
|
+
component_clone_translate_from: "Clona e traduci da %{lang}"
|
|
78
|
+
component_clone_translate_confirm: "Sostituire i campi di questa componente con copie tradotte dalla pagina in %{lang}?"
|
|
73
79
|
component_cloned: Componente clonata con successo
|
|
74
80
|
component_clone_failed: Impossibile clonare la componente
|
|
81
|
+
component_clone_translate_failed: I campi sono stati clonati, ma non è stato possibile avviare la traduzione
|
|
82
|
+
component_clone_translated: Campi tradotti con successo
|
|
75
83
|
component_state_saving: Aggiornamento in corso...
|
|
76
84
|
component_state_saved: Stato aggiornato
|
|
77
85
|
component_state_save_error: Errore durante aggiornamento stato
|
|
@@ -86,19 +94,35 @@ it:
|
|
|
86
94
|
fields_component_missing_alert_html: "Componente <strong>%{component_id}</strong> non trovato. La configurazione del componente potrebbe essere stata rimossa."
|
|
87
95
|
fields_previously_saved_data: "Dati salvati in precedenza:"
|
|
88
96
|
field_file_remove: Rimuovi
|
|
89
|
-
field_file_remove_pending: Il file verra rimosso al salvataggio
|
|
90
|
-
field_file_remove_undo: Annulla
|
|
91
|
-
field_file_new_badge: Nuovo
|
|
92
|
-
field_gallery_empty: Nessuna immagine presente. Aggiungi immagini qui sotto.
|
|
93
|
-
field_gallery_add_images: Aggiungi immagini
|
|
94
|
-
field_gallery_drag_to_reorder: Trascina per riordinare
|
|
95
|
-
field_image_remove: Rimuovi
|
|
96
|
-
field_image_remove_pending: L'immagine verra rimossa al salvataggio
|
|
97
|
-
field_image_new_badge: Nuovo
|
|
98
|
-
field_video_remove: Rimuovi
|
|
99
|
-
field_video_remove_pending: Il video verra rimosso al salvataggio
|
|
100
|
-
field_video_new_badge: Nuovo
|
|
101
97
|
field_multiselect_hint: Tieni premuto Ctrl/Cmd per selezionare piu opzioni
|
|
98
|
+
media_index_title: Media
|
|
99
|
+
media_create_title: Carica media
|
|
100
|
+
media_create_cta: Carica
|
|
101
|
+
media_update_title: Modifica media
|
|
102
|
+
media_alt_text: Testo alternativo
|
|
103
|
+
media_alt_text_regenerate: Rigenera con AI
|
|
104
|
+
media_alt_text_regenerate_confirm: "Il testo alternativo attuale in ogni lingua verrà sostituito con uno generato dall'AI. Continuare?"
|
|
105
|
+
media_alt_text_regenerated: Testo alternativo rigenerato
|
|
106
|
+
media_alt_text_regenerate_unavailable: Il testo alternativo può essere rigenerato solo per le immagini e solo se è configurato un LLM
|
|
107
|
+
media_alt_text_regenerate_failed: Impossibile avviare la rigenerazione del testo alternativo
|
|
108
|
+
media_created: Media caricato con successo
|
|
109
|
+
media_updated: Media aggiornato con successo
|
|
110
|
+
media_deleted: Media eliminato con successo
|
|
111
|
+
media_delete_in_use: "Impossibile eliminare: usato in %{count} punto/i"
|
|
112
|
+
media_delete_failed: Impossibile eliminare il media
|
|
113
|
+
media_choose_from_library: Scegli dalla libreria
|
|
114
|
+
media_picker_title: Libreria media
|
|
115
|
+
media_picker_tab_browse: Sfoglia libreria
|
|
116
|
+
media_picker_tab_upload: Carica nuovo
|
|
117
|
+
media_picker_empty: Nessun media trovato.
|
|
118
|
+
media_picker_confirm: Conferma selezione
|
|
119
|
+
media_picker_selected_label: selezionati
|
|
120
|
+
media_search_placeholder: Cerca media...
|
|
121
|
+
media_upload_cta: Carica
|
|
122
|
+
media_type_all: Tutti
|
|
123
|
+
media_type_image: Immagini
|
|
124
|
+
media_type_video: Video
|
|
125
|
+
media_type_file: File
|
|
102
126
|
field_select_placeholder: "-- Seleziona --"
|
|
103
127
|
field_text_toolbar_bold: Grassetto
|
|
104
128
|
field_text_toolbar_italic: Corsivo
|
data/config/routes.rb
CHANGED
|
@@ -10,6 +10,7 @@ LatoCms::Engine.routes.draw do
|
|
|
10
10
|
|
|
11
11
|
scope :pages do
|
|
12
12
|
get '', to: 'pages#index', as: :pages
|
|
13
|
+
post 'sitemap-export', to: 'pages#sitemap_export_action', as: :pages_sitemap_export_action
|
|
13
14
|
get 'create', to: 'pages#create', as: :pages_create
|
|
14
15
|
post 'create', to: 'pages#create_action', as: :pages_create_action
|
|
15
16
|
get ':id', to: 'pages#show', as: :pages_show
|
|
@@ -23,4 +24,15 @@ LatoCms::Engine.routes.draw do
|
|
|
23
24
|
delete ':id/translations', to: 'pages#unlink_translation_action', as: :pages_unlink_translation_action
|
|
24
25
|
delete ':id', to: 'pages#destroy_action', as: :pages_destroy_action
|
|
25
26
|
end
|
|
27
|
+
|
|
28
|
+
scope :media do
|
|
29
|
+
get '', to: 'media#index', as: :media
|
|
30
|
+
get 'picker', to: 'media#picker_action', as: :media_picker_action
|
|
31
|
+
get 'create', to: 'media#create', as: :media_create
|
|
32
|
+
post 'create', to: 'media#create_action', as: :media_create_action
|
|
33
|
+
get ':id/update', to: 'media#update', as: :media_update
|
|
34
|
+
patch ':id/update', to: 'media#update_action', as: :media_update_action
|
|
35
|
+
post ':id/regenerate-alt-text', to: 'media#regenerate_alt_text_action', as: :media_regenerate_alt_text_action
|
|
36
|
+
delete ':id', to: 'media#destroy_action', as: :media_destroy_action
|
|
37
|
+
end
|
|
26
38
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class CreateLatoCmsMedia < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :lato_cms_media do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :alt_text
|
|
6
|
+
t.string :media_type, null: false, default: 'file'
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
add_index :lato_cms_media, :media_type
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateLatoCmsPageFieldMedia < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :lato_cms_page_field_media do |t|
|
|
4
|
+
t.references :page_field, null: false, foreign_key: { to_table: :lato_cms_page_fields }
|
|
5
|
+
t.references :media, null: false, foreign_key: { to_table: :lato_cms_media }
|
|
6
|
+
t.integer :position, null: false, default: 0
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
add_index :lato_cms_page_field_media, [:page_field_id, :media_id], unique: true
|
|
10
|
+
add_index :lato_cms_page_field_media, [:page_field_id, :position]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class ChangeLatoCmsMediaAltTextToTranslations < ActiveRecord::Migration[8.1]
|
|
2
|
+
class MigrationMedia < ActiveRecord::Base
|
|
3
|
+
self.table_name = 'lato_cms_media'
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def up
|
|
7
|
+
change_column :lato_cms_media, :alt_text, :text
|
|
8
|
+
MigrationMedia.reset_column_information
|
|
9
|
+
|
|
10
|
+
locale = (LatoCms.config.locales.first || :en).to_s
|
|
11
|
+
MigrationMedia.where.not(alt_text: [nil, '']).find_each do |media|
|
|
12
|
+
media.update_column(:alt_text, { locale => media.alt_text }.to_json)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def down
|
|
17
|
+
MigrationMedia.reset_column_information
|
|
18
|
+
|
|
19
|
+
MigrationMedia.find_each do |media|
|
|
20
|
+
translations = JSON.parse(media.alt_text.presence || '{}') rescue {}
|
|
21
|
+
media.update_column(:alt_text, translations.values.compact.first)
|
|
22
|
+
end
|
|
23
|
+
change_column :lato_cms_media, :alt_text, :string
|
|
24
|
+
end
|
|
25
|
+
end
|