lato_cms 3.0.13 → 3.1.1

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/lato_cms/controllers/lato_cms_media_field_controller.js +143 -0
  3. data/app/assets/javascripts/lato_cms/controllers/lato_cms_media_picker_controller.js +110 -0
  4. data/app/assets/javascripts/lato_cms/controllers/lato_cms_media_reorder_controller.js +37 -0
  5. data/app/assets/javascripts/lato_cms/controllers/lato_cms_reload_on_modal_close_controller.js +44 -0
  6. data/app/assets/stylesheets/lato_cms/application.scss +131 -158
  7. data/app/controllers/lato_cms/application_controller.rb +4 -0
  8. data/app/controllers/lato_cms/media_controller.rb +121 -0
  9. data/app/controllers/lato_cms/pages_controller.rb +75 -91
  10. data/app/helpers/lato_cms/media_helper.rb +50 -0
  11. data/app/jobs/lato_cms/export_sitemap_csv_job.rb +33 -0
  12. data/app/jobs/lato_cms/generate_alt_text_job.rb +21 -0
  13. data/app/jobs/lato_cms/generate_video_poster_job.rb +7 -5
  14. data/app/jobs/lato_cms/translate_component_fields_job.rb +16 -0
  15. data/app/models/lato_cms/llm_client.rb +31 -0
  16. data/app/models/lato_cms/media.rb +270 -0
  17. data/app/models/lato_cms/page.rb +40 -0
  18. data/app/models/lato_cms/page_field.rb +52 -80
  19. data/app/models/lato_cms/page_field_media.rb +8 -0
  20. data/app/views/lato_cms/media/_form_create.html.erb +20 -0
  21. data/app/views/lato_cms/media/_form_update.html.erb +60 -0
  22. data/app/views/lato_cms/media/create.html.erb +13 -0
  23. data/app/views/lato_cms/media/index.html.erb +22 -0
  24. data/app/views/lato_cms/media/picker_action.html.erb +98 -0
  25. data/app/views/lato_cms/media/update.html.erb +13 -0
  26. data/app/views/lato_cms/pages/_component_accordion.html.erb +15 -0
  27. data/app/views/lato_cms/pages/fields/_file.html.erb +4 -55
  28. data/app/views/lato_cms/pages/fields/_gallery.html.erb +4 -60
  29. data/app/views/lato_cms/pages/fields/_image.html.erb +4 -68
  30. data/app/views/lato_cms/pages/fields/_media_field.html.erb +62 -0
  31. data/app/views/lato_cms/pages/fields/_media_item.html.erb +27 -0
  32. data/app/views/lato_cms/pages/fields/_video.html.erb +4 -69
  33. data/app/views/lato_cms/pages/index.html.erb +17 -0
  34. data/app/views/lato_cms/pages/show.html.erb +2 -1
  35. data/config/locales/en.yml +36 -12
  36. data/config/locales/it.yml +36 -12
  37. data/config/routes.rb +12 -0
  38. data/db/migrate/20260728175413_create_lato_cms_media.rb +11 -0
  39. data/db/migrate/20260728175414_create_lato_cms_page_field_media.rb +12 -0
  40. data/db/migrate/20260729120000_change_lato_cms_media_alt_text_to_translations.rb +25 -0
  41. data/lib/lato_cms/config.rb +12 -1
  42. data/lib/lato_cms/version.rb +1 -1
  43. metadata +38 -5
  44. data/app/assets/javascripts/lato_cms/controllers/lato_cms_file_field_controller.js +0 -225
  45. data/app/assets/javascripts/lato_cms/controllers/lato_cms_gallery_field_controller.js +0 -190
  46. data/app/assets/javascripts/lato_cms/controllers/lato_cms_image_field_controller.js +0 -169
  47. data/app/assets/javascripts/lato_cms/controllers/lato_cms_video_field_controller.js +0 -180
@@ -1,68 +1,4 @@
1
- <% label = field_config['name'] || field_id.humanize %>
2
- <% required = field_config['required'] == true %>
3
- <% settings = field_config['settings'] || {} %>
4
- <% accept = settings['accept'] || 'image/*' %>
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
- <% label = field_config['name'] || field_id.humanize %>
2
- <% required = field_config['required'] == true %>
3
- <% settings = field_config['settings'] || {} %>
4
- <% accept = settings['accept'] || 'video/*' %>
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>
@@ -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
@@ -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
@@ -3,7 +3,7 @@ module LatoCms
3
3
  # This class contains the default configuration of the engine.
4
4
  ##
5
5
  class Config
6
- attr_accessor :locales, :templates_path, :admin_roles
6
+ attr_accessor :locales, :templates_path, :admin_roles, :llm_api_url, :llm_model, :llm_api_key
7
7
 
8
8
  def initialize
9
9
  @locales = [:en]
@@ -15,6 +15,17 @@ module LatoCms
15
15
  # `operator` has read/edit access; `admin` also manages pages
16
16
  # (create, update, delete) and translation links.
17
17
  @admin_roles = { none: 0, operator: 1, admin: 2 }
18
+
19
+ # Optional OpenAI-compatible endpoint used to auto-generate alt text
20
+ # for uploaded images (see LatoCms::Media#generate_alt_text!). All
21
+ # three must be set for the feature to activate; unset by default.
22
+ @llm_api_url = nil
23
+ @llm_model = nil
24
+ @llm_api_key = nil
25
+ end
26
+
27
+ def llm_configured?
28
+ llm_api_url.present? && llm_model.present? && llm_api_key.present?
18
29
  end
19
30
  end
20
31
  end
@@ -1,3 +1,3 @@
1
1
  module LatoCms
2
- VERSION = "3.0.13"
2
+ VERSION = "3.1.1"
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.13
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregorio Galante
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '1.2'
82
+ - !ruby/object:Gem::Dependency
83
+ name: ruby-vips
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
82
96
  description: A Rails engine to manage application users on Lato projects!
83
97
  email:
84
98
  - me@gregoriogalante.com
@@ -96,26 +110,40 @@ files:
96
110
  - app/assets/javascripts/lato_cms/controllers/lato_cms_component_accordion_controller.js
97
111
  - app/assets/javascripts/lato_cms/controllers/lato_cms_component_form_controller.js
98
112
  - app/assets/javascripts/lato_cms/controllers/lato_cms_component_toggle_controller.js
99
- - app/assets/javascripts/lato_cms/controllers/lato_cms_file_field_controller.js
100
- - app/assets/javascripts/lato_cms/controllers/lato_cms_gallery_field_controller.js
101
- - app/assets/javascripts/lato_cms/controllers/lato_cms_image_field_controller.js
102
113
  - app/assets/javascripts/lato_cms/controllers/lato_cms_json_field_controller.js
114
+ - app/assets/javascripts/lato_cms/controllers/lato_cms_media_field_controller.js
115
+ - app/assets/javascripts/lato_cms/controllers/lato_cms_media_picker_controller.js
116
+ - app/assets/javascripts/lato_cms/controllers/lato_cms_media_reorder_controller.js
103
117
  - app/assets/javascripts/lato_cms/controllers/lato_cms_page_preview_controller.js
118
+ - app/assets/javascripts/lato_cms/controllers/lato_cms_reload_on_modal_close_controller.js
104
119
  - app/assets/javascripts/lato_cms/controllers/lato_cms_repeater_controller.js
105
120
  - app/assets/javascripts/lato_cms/controllers/lato_cms_text_field_controller.js
106
- - app/assets/javascripts/lato_cms/controllers/lato_cms_video_field_controller.js
107
121
  - app/assets/stylesheets/lato_cms/application.scss
108
122
  - app/controllers/lato_cms/api/pages_controller.rb
109
123
  - app/controllers/lato_cms/application_controller.rb
124
+ - app/controllers/lato_cms/media_controller.rb
110
125
  - app/controllers/lato_cms/pages_controller.rb
111
126
  - app/helpers/lato_cms/application_helper.rb
127
+ - app/helpers/lato_cms/media_helper.rb
112
128
  - app/helpers/lato_cms/pages_helper.rb
113
129
  - app/jobs/lato_cms/application_job.rb
130
+ - app/jobs/lato_cms/export_sitemap_csv_job.rb
131
+ - app/jobs/lato_cms/generate_alt_text_job.rb
114
132
  - app/jobs/lato_cms/generate_video_poster_job.rb
133
+ - app/jobs/lato_cms/translate_component_fields_job.rb
115
134
  - app/mailers/lato_cms/application_mailer.rb
135
+ - app/models/lato_cms/llm_client.rb
136
+ - app/models/lato_cms/media.rb
116
137
  - app/models/lato_cms/page.rb
117
138
  - app/models/lato_cms/page_field.rb
139
+ - app/models/lato_cms/page_field_media.rb
118
140
  - app/models/lato_cms/template_manager.rb
141
+ - app/views/lato_cms/media/_form_create.html.erb
142
+ - app/views/lato_cms/media/_form_update.html.erb
143
+ - app/views/lato_cms/media/create.html.erb
144
+ - app/views/lato_cms/media/index.html.erb
145
+ - app/views/lato_cms/media/picker_action.html.erb
146
+ - app/views/lato_cms/media/update.html.erb
119
147
  - app/views/lato_cms/pages/_component_accordion.html.erb
120
148
  - app/views/lato_cms/pages/_fields_editor.html.erb
121
149
  - app/views/lato_cms/pages/_form_create.html.erb
@@ -132,6 +160,8 @@ files:
132
160
  - app/views/lato_cms/pages/fields/_gallery.html.erb
133
161
  - app/views/lato_cms/pages/fields/_image.html.erb
134
162
  - app/views/lato_cms/pages/fields/_json.html.erb
163
+ - app/views/lato_cms/pages/fields/_media_field.html.erb
164
+ - app/views/lato_cms/pages/fields/_media_item.html.erb
135
165
  - app/views/lato_cms/pages/fields/_multiselect.html.erb
136
166
  - app/views/lato_cms/pages/fields/_number.html.erb
137
167
  - app/views/lato_cms/pages/fields/_select.html.erb
@@ -152,6 +182,9 @@ files:
152
182
  - db/migrate/20260323171241_create_lato_cms_page_fields.rb
153
183
  - db/migrate/20260705000000_add_translation_group_id_to_lato_cms_pages.rb
154
184
  - db/migrate/20260712092241_change_lato_cms_admin_to_lato_cms_admin_role_on_lato_user.rb
185
+ - db/migrate/20260728175413_create_lato_cms_media.rb
186
+ - db/migrate/20260728175414_create_lato_cms_page_field_media.rb
187
+ - db/migrate/20260729120000_change_lato_cms_media_alt_text_to_translations.rb
155
188
  - lib/lato_cms.rb
156
189
  - lib/lato_cms/config.rb
157
190
  - lib/lato_cms/engine.rb