lato_cms 3.0.5 → 3.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 880a42ede332f95018d49081a9d2dc3ac0b510c62e4a218ebf71561eec3c9408
4
- data.tar.gz: 9e7f487b6323f950e3d68bf80acc3bb9770962e72c1f64fc6394ab0adf66e368
3
+ metadata.gz: cbcd0c46e947f128bf5c6334e7729fbbbe12682c3b2e89d4442bb7b1a73528a4
4
+ data.tar.gz: 040ad879939068be79b8078a3a011a40f49f8a35f7539e9d791904b4148df4c2
5
5
  SHA512:
6
- metadata.gz: 198521b168caa3bfb06b67cb466335da752144c8d33ad8d7850d70b36c4b598864387d1cff4bc9fb0f389fbed304093f49446a944d6e483917abfe98bc59d825
7
- data.tar.gz: 657818b8115590944db38ea1ba4141dd2f720455ed4621a7ff4ad046ac753d0bdf16c01c1a39df79cf251f28c81b515f1316c31dbf0f92c4166725b6fc12fb75
6
+ metadata.gz: 1644c351792d4ced7526999d47f250ebb09b3fd5f9de897818465bfa37e00b7623af635309a589b5fb8001ef5d7021c11f7e854738c6054eb719398dc46f8d98
7
+ data.tar.gz: 39131b5dd434621e95a147d27287c0479e06ca42f086f73906f185e0a0b64086bc134234315400dc56a6ef20dfab49a867a102eec522f4d41c5e986c9cf9c2f0
@@ -125,6 +125,56 @@ module LatoCms
125
125
  end
126
126
  end
127
127
 
128
+ def clone_component_action
129
+ @page = query_pages.find(params[:id])
130
+ source = @page.translations.find_by(id: params[:source_page_id])
131
+ template_component_id = params[:template_component_id].to_s
132
+
133
+ respond_to do |format|
134
+ if source && clone_component_fields(source, @page, template_component_id)
135
+ format.html { redirect_to lato_cms.pages_show_path(@page), notice: t('lato_cms.component_cloned') }
136
+ format.json { render json: { message: t('lato_cms.component_cloned') } }
137
+ else
138
+ format.html { redirect_to lato_cms.pages_show_path(@page), alert: t('lato_cms.component_clone_failed') }
139
+ format.json { render json: { error: t('lato_cms.component_clone_failed') }, status: :unprocessable_entity }
140
+ end
141
+ end
142
+ end
143
+
144
+ def translations
145
+ @page = query_pages.find(params[:id])
146
+ end
147
+
148
+ def link_translation_action
149
+ @page = query_pages.find(params[:id])
150
+ other_page = query_pages.find(params[:translation_page_id])
151
+
152
+ respond_to do |format|
153
+ if @page.link_translation(other_page)
154
+ format.html { redirect_to lato_cms.pages_translations_path(@page), notice: t('lato_cms.translation_linked') }
155
+ format.json { render json: @page }
156
+ else
157
+ format.html { redirect_to lato_cms.pages_translations_path(@page), alert: t('lato_cms.translation_link_failed') }
158
+ format.json { render json: { error: t('lato_cms.translation_link_failed') }, status: :unprocessable_entity }
159
+ end
160
+ end
161
+ end
162
+
163
+ def unlink_translation_action
164
+ @page = query_pages.find(params[:id])
165
+ other_page = query_pages.find(params[:translation_page_id])
166
+
167
+ respond_to do |format|
168
+ if @page.unlink_translation(other_page)
169
+ format.html { redirect_to lato_cms.pages_translations_path(@page), notice: t('lato_cms.translation_unlinked') }
170
+ format.json { render json: @page }
171
+ else
172
+ format.html { redirect_to lato_cms.pages_translations_path(@page), alert: t('lato_cms.translation_unlink_failed') }
173
+ format.json { render json: { error: t('lato_cms.translation_unlink_failed') }, status: :unprocessable_entity }
174
+ end
175
+ end
176
+ end
177
+
128
178
  def destroy_action
129
179
  @page = query_pages.find(params[:id])
130
180
 
@@ -141,6 +191,33 @@ module LatoCms
141
191
 
142
192
  private
143
193
 
194
+ # Replaces the target page's fields for a component with copies of the source
195
+ # page's fields for the same component. Attachments share the source blobs.
196
+ def clone_component_fields(source, target, template_component_id)
197
+ # Components are template-specific: only clone between pages using the same template.
198
+ return false unless source.template_id == target.template_id
199
+
200
+ source_fields = source.fields.select { |f| f.template_component_id == template_component_id }
201
+ return false if source_fields.empty?
202
+
203
+ target.transaction do
204
+ target.fields.where(template_component_id: template_component_id).destroy_all
205
+
206
+ source_fields.each do |src|
207
+ field = target.fields.create!(
208
+ template_id: target.template_id,
209
+ template_component_id: src.template_component_id,
210
+ component_id: src.component_id,
211
+ field_id: src.field_id,
212
+ value: src.value
213
+ )
214
+ src.files.each { |file| field.files.attach(file.blob) }
215
+ end
216
+ end
217
+
218
+ true
219
+ end
220
+
144
221
  def save_repeater_fields(template_component, component, repeater_items, repeater_order, errors)
145
222
  template_component_id = template_component[:template_component_id]
146
223
  component_id = template_component[:component_id]
@@ -10,6 +10,7 @@ module LatoCms
10
10
  '#'
11
11
  end
12
12
 
13
+ # Index locale cell (also the `locale` column viewer). Static badge.
13
14
  def lato_cms_page_locale(page)
14
15
  content_tag(:span, class: 'badge bg-secondary') do
15
16
  concat locale_to_flag(page.locale)
@@ -17,6 +18,7 @@ module LatoCms
17
18
  end
18
19
  end
19
20
 
21
+ # Index actions cell (also the `actions` column viewer). Inline buttons.
20
22
  def lato_cms_page_actions(page, show_edit: false, show_delete: false, hide_show: false)
21
23
  btn_group = capture do
22
24
  content_tag(:div, class: 'btn-group btn-group-sm') do
@@ -42,6 +44,63 @@ module LatoCms
42
44
  end
43
45
  end
44
46
 
47
+ # Show-page locale: dropdown to jump to the same page in another language.
48
+ def lato_cms_page_locale_dropdown(page)
49
+ siblings = page.translations.order(:locale).to_a
50
+ badge = capture do
51
+ concat locale_to_flag(page.locale)
52
+ concat " #{page.locale.upcase}"
53
+ end
54
+
55
+ return content_tag(:span, badge, class: 'badge bg-secondary') if siblings.empty?
56
+
57
+ content_tag(:div, class: 'dropdown') do
58
+ concat button_tag(badge, type: 'button', class: 'btn btn-sm btn-secondary dropdown-toggle', data: { bs_toggle: 'dropdown' })
59
+ concat(content_tag(:ul, class: 'dropdown-menu') do
60
+ siblings.each do |sibling|
61
+ concat(content_tag(:li) do
62
+ link_to lato_cms.pages_show_path(sibling), class: 'dropdown-item' do
63
+ concat locale_to_flag(sibling.locale)
64
+ concat " #{sibling.locale.upcase} — #{sibling.title}"
65
+ end
66
+ end)
67
+ end
68
+ end)
69
+ end
70
+ end
71
+
72
+ # Show-page actions: single dropdown (view frontend, show, edit settings,
73
+ # translations, delete).
74
+ def lato_cms_page_actions_dropdown(page, show_edit: false, show_translations: false, show_delete: false, hide_show: false)
75
+ content_tag(:div, class: 'dropdown', title: t('lato_cms.cta_actions'), data: { controller: 'lato-tooltip' }) do
76
+ concat button_tag(tag.i(class: 'bi bi-three-dots-vertical'), type: 'button',
77
+ class: 'btn btn-sm btn-outline-primary', data: { bs_toggle: 'dropdown' })
78
+ concat(content_tag(:ul, class: 'dropdown-menu dropdown-menu-end') do
79
+ if page.frontend_url.present?
80
+ concat content_tag(:li, link_to(t('lato_cms.action_view_frontend'), page.frontend_url, class: 'dropdown-item', target: '_blank'))
81
+ else
82
+ concat content_tag(:li, content_tag(:span, t('lato_cms.action_view_frontend'), class: 'dropdown-item disabled text-muted'))
83
+ end
84
+ unless hide_show
85
+ concat content_tag(:li, link_to(t('lato_cms.cta_show'), lato_cms.pages_show_path(page), class: 'dropdown-item'))
86
+ end
87
+ if show_edit
88
+ concat content_tag(:li, link_to(t('lato_cms.cta_edit'), lato_cms.pages_update_path(page), class: 'dropdown-item',
89
+ data: { lato_action_target: 'trigger', turbo_frame: dom_id(page, 'form'), action_title: t('lato_cms.page_update_title') }))
90
+ end
91
+ if show_translations
92
+ concat content_tag(:li, link_to(t('lato_cms.translations_title'), lato_cms.pages_translations_path(page), class: 'dropdown-item',
93
+ data: { lato_action_target: 'trigger', turbo_frame: dom_id(page, 'translations'), action_title: t('lato_cms.translations_title') }))
94
+ end
95
+ if show_delete
96
+ concat content_tag(:li, tag.hr(class: 'dropdown-divider'))
97
+ concat content_tag(:li, link_to(t('lato_cms.cta_delete'), lato_cms.pages_destroy_action_path(page), class: 'dropdown-item text-danger',
98
+ data: { turbo_method: 'DELETE', turbo_confirm: t('lato_cms.cta_delete_confirm') }))
99
+ end
100
+ end)
101
+ end
102
+ end
103
+
45
104
  def lato_cms_render_field(field_id:, field_config:, page_field:, input_name_prefix: nil, dom_id_prefix: nil)
46
105
  render resolve_lato_cms_field_partial(field_config),
47
106
  field_id: field_id,
@@ -10,11 +10,73 @@ module LatoCms
10
10
  validates :locale, presence: true, inclusion: { in: ->(page) { LatoCms.config.locales.map(&:to_s) } }
11
11
  validates :permalink, presence: true, format: { with: /\A\/([a-zA-Z0-9\-_]+(\/[a-zA-Z0-9\-_]+)*)?\z/, message: 'must start with / and contain only letters, numbers, hyphens, and underscores' }
12
12
  validate :permalink_unique_within_lato_spaces_group
13
+ validate :locale_unique_within_translation_group
13
14
 
14
15
  before_validation :generate_permalink, on: :create
15
16
 
16
17
  has_many :fields, class_name: 'LatoCms::PageField', dependent: :destroy
17
18
 
19
+ # ── Translations ────────────────────────────────────────────────────────
20
+ # Pages sharing the same translation_group_id (within the same spaces group)
21
+ # are translations of each other, at most one per locale.
22
+
23
+ # Sibling pages linked as translations of this page.
24
+ def translations
25
+ return LatoCms::Page.none if translation_group_id.blank?
26
+
27
+ LatoCms::Page
28
+ .for_lato_spaces_group(lato_spaces_group_id)
29
+ .where(translation_group_id: translation_group_id)
30
+ .where.not(id: id)
31
+ end
32
+
33
+ # The linked translation for a given locale, if any.
34
+ def translation_for(locale)
35
+ translations.find_by(locale: locale.to_s)
36
+ end
37
+
38
+ # Pages of a given locale that can be linked to this page: same spaces group,
39
+ # not this page, and not already part of any translation group.
40
+ def translation_candidates(locale)
41
+ LatoCms::Page
42
+ .for_lato_spaces_group(lato_spaces_group_id)
43
+ .where(locale: locale.to_s, translation_group_id: nil)
44
+ .where.not(id: id)
45
+ end
46
+
47
+ # Links another page as a translation of this one, merging any existing
48
+ # groups so all members share a single translation_group_id.
49
+ def link_translation(other_page)
50
+ return false unless other_page && other_page.id != id
51
+ return false unless other_page.lato_spaces_group_id == lato_spaces_group_id
52
+
53
+ group_id = translation_group_id.presence || other_page.translation_group_id.presence || SecureRandom.uuid
54
+ old_group_id = other_page.translation_group_id.presence
55
+
56
+ transaction do
57
+ update!(translation_group_id: group_id)
58
+ # Re-point every member of the other page's former group to keep groups merged.
59
+ if old_group_id && old_group_id != group_id
60
+ LatoCms::Page
61
+ .for_lato_spaces_group(lato_spaces_group_id)
62
+ .where(translation_group_id: old_group_id)
63
+ .update_all(translation_group_id: group_id)
64
+ else
65
+ other_page.update!(translation_group_id: group_id)
66
+ end
67
+ end
68
+
69
+ true
70
+ end
71
+
72
+ # Removes a page from this page's translation group.
73
+ def unlink_translation(other_page)
74
+ return false unless other_page && other_page.translation_group_id == translation_group_id
75
+
76
+ other_page.update!(translation_group_id: nil)
77
+ true
78
+ end
79
+
18
80
  def template
19
81
  LatoCms::TemplateManager.find_template(template_id)
20
82
  end
@@ -64,6 +126,7 @@ module LatoCms
64
126
  template_id: template_id,
65
127
  template_name: template_name,
66
128
  frontend_url: frontend_url,
129
+ translations: translations_json,
67
130
  created_at: created_at,
68
131
  updated_at: updated_at
69
132
  }
@@ -78,6 +141,13 @@ module LatoCms
78
141
 
79
142
  private
80
143
 
144
+ # Map of locale => permalink/id for linked translations, for API consumers.
145
+ def translations_json
146
+ translations.each_with_object({}) do |page, hash|
147
+ hash[page.locale] = { id: page.id, permalink: page.permalink, frontend_url: page.frontend_url }
148
+ end
149
+ end
150
+
81
151
  def build_fields_json(comps)
82
152
  fields_by_component = fields.group_by(&:template_component_id)
83
153
 
@@ -160,5 +230,17 @@ module LatoCms
160
230
  errors.add(:permalink, :taken)
161
231
  end
162
232
  end
233
+
234
+ def locale_unique_within_translation_group
235
+ return if translation_group_id.blank?
236
+
237
+ conflict = LatoCms::Page
238
+ .for_lato_spaces_group(lato_spaces_group_id)
239
+ .where(translation_group_id: translation_group_id, locale: locale)
240
+ .where.not(id: id)
241
+ .exists?
242
+
243
+ errors.add(:locale, :taken) if conflict
244
+ end
163
245
  end
164
246
  end
@@ -2,6 +2,9 @@
2
2
  <% component_fields = page.fields.select { |f| f.template_component_id == tc[:template_component_id] } %>
3
3
  <% required_component = tc[:required] == true %>
4
4
  <% enabled_component = page.component_effectively_enabled?(tc[:template_component_id]) %>
5
+ <%# Clonable translations: linked pages using the same template (components differ
6
+ across templates, so cross-template clone is not allowed). %>
7
+ <% clonable_translations = tc[:component] ? translations.select { |s| s.template_id == page.template_id } : [] %>
5
8
 
6
9
  <div class="accordion-item">
7
10
  <h2 class="accordion-header" id="heading-<%= accordion_id %>">
@@ -23,25 +26,50 @@
23
26
  data-lato-cms-component-toggle-saving-label-value="<%= t('lato_cms.component_state_saving') %>"
24
27
  data-lato-cms-component-toggle-saved-label-value="<%= t('lato_cms.component_state_saved') %>"
25
28
  data-lato-cms-component-toggle-error-label-value="<%= t('lato_cms.component_state_save_error') %>">
29
+ <% end %>
26
30
 
27
- <%= form_with url: lato_cms.pages_toggle_component_action_path(page, tc[:template_component_id]), method: :patch,
28
- data: { turbo: false, action: 'submit->lato-cms-component-toggle#toggle' } do %>
29
- <div class="d-flex justify-content-between align-items-center mb-3 pb-2 border-bottom">
30
- <div class="d-flex align-items-center gap-2 small fw-medium <%= enabled_component ? 'text-success' : 'text-muted' %>" data-lato-cms-component-toggle-target="stateWrapper">
31
- <i class="<%= enabled_component ? 'bi bi-eye' : 'bi bi-eye-slash' %>" data-lato-cms-component-toggle-target="stateIcon"></i>
32
- <span data-lato-cms-component-toggle-target="stateLabel"><%= enabled_component ? t('lato_cms.component_active_label') : t('lato_cms.component_inactive_label') %></span>
31
+ <%# Component settings header: enable/disable toggle (left) and clone (right). %>
32
+ <% if !required_component || clonable_translations.any? %>
33
+ <div class="d-flex justify-content-between align-items-center gap-2 mb-3 pb-2 border-bottom">
34
+ <div class="d-flex align-items-center gap-3">
35
+ <% unless required_component %>
36
+ <%= form_with url: lato_cms.pages_toggle_component_action_path(page, tc[:template_component_id]), method: :patch,
37
+ data: { turbo: false, action: 'submit->lato-cms-component-toggle#toggle' } do %>
38
+ <div class="d-flex align-items-center gap-3">
39
+ <div class="form-check form-switch m-0">
40
+ <%= hidden_field_tag :enabled, '0' %>
41
+ <%= check_box_tag :enabled, '1', enabled_component, class: 'form-check-input', id: "toggle-#{accordion_id}", onchange: 'this.form.requestSubmit()', 'aria-label': t('lato_cms.component_enabled_label'), data: { lato_cms_component_toggle_target: 'checkbox' } %>
42
+ </div>
43
+ <div class="d-flex align-items-center gap-2 small fw-medium <%= enabled_component ? 'text-success' : 'text-muted' %>" data-lato-cms-component-toggle-target="stateWrapper">
44
+ <i class="<%= enabled_component ? 'bi bi-eye' : 'bi bi-eye-slash' %>" data-lato-cms-component-toggle-target="stateIcon"></i>
45
+ <span data-lato-cms-component-toggle-target="stateLabel"><%= enabled_component ? t('lato_cms.component_active_label') : t('lato_cms.component_inactive_label') %></span>
46
+ </div>
47
+ <span data-lato-cms-component-toggle-target="status"></span>
48
+ </div>
49
+ <% end %>
50
+ <% end %>
33
51
  </div>
34
- <div class="d-flex align-items-center gap-2">
35
- <span data-lato-cms-component-toggle-target="status"></span>
36
- <div class="form-check form-switch m-0">
37
- <%= hidden_field_tag :enabled, '0' %>
38
- <%= check_box_tag :enabled, '1', enabled_component, class: 'form-check-input', id: "toggle-#{accordion_id}", onchange: 'this.form.requestSubmit()', data: { lato_cms_component_toggle_target: 'checkbox' } %>
39
- <%= label_tag "toggle-#{accordion_id}", t('lato_cms.component_enabled_label'), class: 'form-check-label ms-1 small' %>
52
+
53
+ <% if clonable_translations.any? %>
54
+ <div class="dropdown flex-shrink-0" data-controller="lato-tooltip" title="<%= t('lato_cms.component_clone_cta') %>">
55
+ <button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="dropdown" aria-expanded="false">
56
+ <i class="bi bi-files"></i>
57
+ </button>
58
+ <ul class="dropdown-menu dropdown-menu-end">
59
+ <% clonable_translations.each do |sibling| %>
60
+ <li>
61
+ <%= button_to lato_cms.pages_clone_component_action_path(page, tc[:template_component_id]),
62
+ params: { source_page_id: sibling.id }, method: :post, class: 'dropdown-item',
63
+ form: { data: { turbo_confirm: t('lato_cms.component_clone_confirm', lang: sibling.locale.upcase) } } do %>
64
+ <%= locale_to_flag(sibling.locale) %> <%= t('lato_cms.component_clone_from', lang: sibling.locale.upcase) %>
65
+ <% end %>
66
+ </li>
67
+ <% end %>
68
+ </ul>
40
69
  </div>
41
- </div>
70
+ <% end %>
42
71
  </div>
43
72
  <% end %>
44
- <% end %>
45
73
 
46
74
  <% if tc[:component] %>
47
75
  <% if required_component %>
@@ -1,5 +1,6 @@
1
1
  <% page ||= @page %>
2
2
  <% template_components ||= @template_components %>
3
+ <% translations = page.translations.order(:locale).to_a %>
3
4
 
4
5
  <div class="accordion" id="components-accordion" data-controller="lato-cms-component-accordion">
5
6
  <% template_components.each_with_index do |tc, index| %>
@@ -7,6 +8,7 @@
7
8
  page: page,
8
9
  tc: tc,
9
10
  index: index,
10
- expanded: index == 0 %>
11
+ expanded: index == 0,
12
+ translations: translations %>
11
13
  <% end %>
12
14
  </div>
@@ -0,0 +1,41 @@
1
+ <%= turbo_frame_tag dom_id(page, 'translations') do %>
2
+ <%= lato_form_notices class: %w[mb-3] %>
3
+
4
+ <% other_locales = LatoCms.config.locales.map(&:to_s) - [page.locale.to_s] %>
5
+ <% if other_locales.empty? %>
6
+ <p class="text-muted mb-0"><%= t('lato_cms.translation_no_other_locales') %></p>
7
+ <% else %>
8
+ <% other_locales.each do |locale| %>
9
+ <div class="d-flex align-items-center justify-content-between gap-2 mb-2">
10
+ <span class="badge bg-secondary">
11
+ <%= locale_to_flag(locale) %> <%= locale.upcase %>
12
+ </span>
13
+
14
+ <% sibling = page.translation_for(locale) %>
15
+ <% if sibling %>
16
+ <%# Linked translation: show it with an unlink button. %>
17
+ <div class="d-flex align-items-center gap-2 flex-grow-1 justify-content-end">
18
+ <%= link_to sibling.title, lato_cms.pages_show_path(sibling), class: 'text-truncate' %>
19
+ <%= button_to t('lato_cms.translation_unlink_cta'), lato_cms.pages_unlink_translation_action_path(page),
20
+ method: :delete, params: { translation_page_id: sibling.id },
21
+ class: 'btn btn-sm btn-outline-danger', form: { data: { turbo_confirm: t('lato_cms.translation_unlink_confirm') } } %>
22
+ </div>
23
+ <% else %>
24
+ <%# No translation for this locale: let the user link an existing page. %>
25
+ <% candidates = page.translation_candidates(locale) %>
26
+ <% if candidates.any? %>
27
+ <%= form_with url: lato_cms.pages_link_translation_action_path(page), method: :post,
28
+ class: 'd-flex align-items-center gap-2 flex-grow-1 justify-content-end' do %>
29
+ <%= select_tag :translation_page_id,
30
+ options_from_collection_for_select(candidates, :id, :title),
31
+ class: 'form-select form-select-sm', style: 'max-width: 260px;' %>
32
+ <%= submit_tag t('lato_cms.translation_link_cta'), class: 'btn btn-sm btn-outline-primary' %>
33
+ <% end %>
34
+ <% else %>
35
+ <span class="text-muted small"><%= t('lato_cms.translation_no_candidates') %></span>
36
+ <% end %>
37
+ <% end %>
38
+ </div>
39
+ <% end %>
40
+ <% end %>
41
+ <% end %>
@@ -33,24 +33,24 @@
33
33
  <div class="card-header d-flex justify-content-between align-items-center">
34
34
  <h2 class="fs-5 mb-0"><%= t('lato_cms.show_editor_title') %></h2>
35
35
  <div class="d-flex align-items-center gap-2">
36
- <%= lato_cms_page_locale(@page) %>
36
+ <%= lato_cms_page_locale_dropdown(@page) %>
37
37
  <span class="vr"></span>
38
- <%= lato_cms_page_actions(@page, show_edit: true, show_delete: true, hide_show: true) %>
38
+ <%= lato_cms_page_actions_dropdown(@page, show_edit: true, show_translations: true, show_delete: true, hide_show: true) %>
39
39
  <span class="vr"></span>
40
- <button class="btn btn-sm btn-outline-secondary"
41
- title="<%= t('lato_cms.show_advanced_editor_tooltip') %>"
42
- data-controller="lato-tooltip"
43
- data-action="click->lato-cms-advanced-editor#toggle"
44
- data-lato-cms-advanced-editor-target="toggleBtn">
45
- <i class="bi bi-fullscreen"></i>
46
- </button>
47
- <button class="btn btn-sm btn-outline-secondary d-none"
40
+ <button class="btn btn-sm btn-outline-primary d-none"
48
41
  title="<%= t('lato_cms.show_hide_panel_tooltip') %>"
49
42
  data-controller="lato-tooltip"
50
43
  data-action="click->lato-cms-advanced-editor#toggleSidebar"
51
44
  data-lato-cms-advanced-editor-target="collapseBtn">
52
45
  <i class="bi bi-layout-sidebar-reverse"></i>
53
46
  </button>
47
+ <button class="btn btn-sm btn-outline-primary"
48
+ title="<%= t('lato_cms.show_advanced_editor_tooltip') %>"
49
+ data-controller="lato-tooltip"
50
+ data-action="click->lato-cms-advanced-editor#toggle"
51
+ data-lato-cms-advanced-editor-target="toggleBtn">
52
+ <i class="bi bi-fullscreen"></i>
53
+ </button>
54
54
  </div>
55
55
  </div>
56
56
  <div class="card-body" style="overflow-y: auto; max-height: calc(100dvh - 230px);">
@@ -0,0 +1,14 @@
1
+ <%= lato_page_head t('lato_cms.translations_title'), [
2
+ { label: t('lato_cms.pages_index_title'), path: lato_cms.pages_path },
3
+ { label: @page.title, path: lato_cms.pages_show_path(@page) },
4
+ { label: t('lato_cms.translations_title') }
5
+ ] %>
6
+
7
+ <div class="card mb-4">
8
+ <div class="card-header">
9
+ <h2 class="fs-4 mb-0"><%= t('lato_cms.translations_title') %></h2>
10
+ </div>
11
+ <div class="card-body">
12
+ <%= render 'lato_cms/pages/translations', page: @page %>
13
+ </div>
14
+ </div>
@@ -23,6 +23,17 @@ en:
23
23
  form_no_template_option: "-- No template --"
24
24
  form_template_unavailable: "The template \"%{template_id}\" is no longer available. Please select a different template."
25
25
  action_view_frontend: View
26
+ cta_actions: Actions
27
+ translations_title: Translations
28
+ translation_no_other_locales: No other locales are configured.
29
+ translation_link_cta: Link
30
+ translation_unlink_cta: Unlink
31
+ translation_unlink_confirm: Are you sure you want to unlink this translation?
32
+ translation_no_candidates: No pages available for this language
33
+ translation_linked: Translation linked successfully
34
+ translation_unlinked: Translation unlinked successfully
35
+ translation_link_failed: Failed to link translation
36
+ translation_unlink_failed: Failed to unlink translation
26
37
  show_no_preview_title: No preview available
27
38
  show_no_preview_description: Set the <strong>Frontend URL</strong> to see the page preview.
28
39
  show_edit_settings_cta: Edit page settings
@@ -42,6 +53,11 @@ en:
42
53
  component_inactive_label: This component is not active
43
54
  component_disabled_alert: This component is disabled and will not be exposed by API.
44
55
  component_state_updated: Component state updated
56
+ component_clone_cta: Clone
57
+ component_clone_from: "Clone from %{lang}"
58
+ component_clone_confirm: "Replace this component's fields with the ones from the %{lang} page?"
59
+ component_cloned: Component cloned successfully
60
+ component_clone_failed: Failed to clone component
45
61
  component_state_saving: Updating...
46
62
  component_state_saved: State updated
47
63
  component_state_save_error: Failed to update state
@@ -23,6 +23,17 @@ it:
23
23
  form_no_template_option: "-- Nessun template --"
24
24
  form_template_unavailable: "Il template \"%{template_id}\" non e piu disponibile. Seleziona un template diverso."
25
25
  action_view_frontend: Anteprima
26
+ cta_actions: Azioni
27
+ translations_title: Traduzioni
28
+ translation_no_other_locales: Nessuna altra lingua configurata.
29
+ translation_link_cta: Collega
30
+ translation_unlink_cta: Scollega
31
+ translation_unlink_confirm: Sei sicuro di voler scollegare questa traduzione?
32
+ translation_no_candidates: Nessuna pagina disponibile per questa lingua
33
+ translation_linked: Traduzione collegata con successo
34
+ translation_unlinked: Traduzione scollegata con successo
35
+ translation_link_failed: Impossibile collegare la traduzione
36
+ translation_unlink_failed: Impossibile scollegare la traduzione
26
37
  show_no_preview_title: Anteprima non disponibile
27
38
  show_no_preview_description: Imposta il <strong>URL Frontend</strong> per visualizzare l'anteprima della pagina.
28
39
  show_edit_settings_cta: Modifica impostazioni pagina
@@ -42,6 +53,11 @@ it:
42
53
  component_inactive_label: Questa componente non e attiva
43
54
  component_disabled_alert: Questa componente e disattivata e non verra esposta dalle API.
44
55
  component_state_updated: Stato componente aggiornato
56
+ component_clone_cta: Clona
57
+ component_clone_from: "Clona da %{lang}"
58
+ component_clone_confirm: "Sostituire i campi di questa componente con quelli della pagina in %{lang}?"
59
+ component_cloned: Componente clonata con successo
60
+ component_clone_failed: Impossibile clonare la componente
45
61
  component_state_saving: Aggiornamento in corso...
46
62
  component_state_saved: Stato aggiornato
47
63
  component_state_save_error: Errore durante aggiornamento stato
data/config/routes.rb CHANGED
@@ -17,6 +17,10 @@ LatoCms::Engine.routes.draw do
17
17
  patch ':id/update', to: 'pages#update_action', as: :pages_update_action
18
18
  post ':id/fields', to: 'pages#save_fields_action', as: :pages_save_fields_action
19
19
  patch ':id/components/:template_component_id/toggle', to: 'pages#toggle_component_action', as: :pages_toggle_component_action
20
+ post ':id/components/:template_component_id/clone', to: 'pages#clone_component_action', as: :pages_clone_component_action
21
+ get ':id/translations', to: 'pages#translations', as: :pages_translations
22
+ post ':id/translations', to: 'pages#link_translation_action', as: :pages_link_translation_action
23
+ delete ':id/translations', to: 'pages#unlink_translation_action', as: :pages_unlink_translation_action
20
24
  delete ':id', to: 'pages#destroy_action', as: :pages_destroy_action
21
25
  end
22
26
  end
@@ -0,0 +1,8 @@
1
+ class AddTranslationGroupIdToLatoCmsPages < ActiveRecord::Migration[8.1]
2
+ def change
3
+ # Pages sharing the same translation_group_id are translations of each other
4
+ # (one page per locale within a group). Nil means the page is not linked.
5
+ add_column :lato_cms_pages, :translation_group_id, :string
6
+ add_index :lato_cms_pages, :translation_group_id
7
+ end
8
+ end
@@ -1,3 +1,3 @@
1
1
  module LatoCms
2
- VERSION = "3.0.5"
2
+ VERSION = "3.0.6"
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.5
4
+ version: 3.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregorio Galante
@@ -120,6 +120,7 @@ files:
120
120
  - app/views/lato_cms/pages/_form_update.html.erb
121
121
  - app/views/lato_cms/pages/_repeater.html.erb
122
122
  - app/views/lato_cms/pages/_repeater_item.html.erb
123
+ - app/views/lato_cms/pages/_translations.html.erb
123
124
  - app/views/lato_cms/pages/create.html.erb
124
125
  - app/views/lato_cms/pages/fields/_boolean.html.erb
125
126
  - app/views/lato_cms/pages/fields/_color.html.erb
@@ -137,6 +138,7 @@ files:
137
138
  - app/views/lato_cms/pages/fields/_textarea.html.erb
138
139
  - app/views/lato_cms/pages/index.html.erb
139
140
  - app/views/lato_cms/pages/show.html.erb
141
+ - app/views/lato_cms/pages/translations.html.erb
140
142
  - app/views/lato_cms/pages/update.html.erb
141
143
  - config/importmap.rb
142
144
  - config/locales/en.yml
@@ -145,6 +147,7 @@ files:
145
147
  - db/migrate/20250328072343_add_lato_cms_admin_to_lato_user.rb
146
148
  - db/migrate/20260320070819_create_lato_cms_pages.rb
147
149
  - db/migrate/20260323171241_create_lato_cms_page_fields.rb
150
+ - db/migrate/20260705000000_add_translation_group_id_to_lato_cms_pages.rb
148
151
  - lib/lato_cms.rb
149
152
  - lib/lato_cms/config.rb
150
153
  - lib/lato_cms/engine.rb