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 +4 -4
- data/app/controllers/lato_cms/pages_controller.rb +77 -0
- data/app/helpers/lato_cms/pages_helper.rb +59 -0
- data/app/models/lato_cms/page.rb +82 -0
- data/app/views/lato_cms/pages/_component_accordion.html.erb +42 -14
- data/app/views/lato_cms/pages/_fields_editor.html.erb +3 -1
- data/app/views/lato_cms/pages/_translations.html.erb +41 -0
- data/app/views/lato_cms/pages/show.html.erb +10 -10
- data/app/views/lato_cms/pages/translations.html.erb +14 -0
- data/config/locales/en.yml +16 -0
- data/config/locales/it.yml +16 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20260705000000_add_translation_group_id_to_lato_cms_pages.rb +8 -0
- data/lib/lato_cms/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbcd0c46e947f128bf5c6334e7729fbbbe12682c3b2e89d4442bb7b1a73528a4
|
|
4
|
+
data.tar.gz: 040ad879939068be79b8078a3a011a40f49f8a35f7539e9d791904b4148df4c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
data/app/models/lato_cms/page.rb
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
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-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
<div class="
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
<%=
|
|
36
|
+
<%= lato_cms_page_locale_dropdown(@page) %>
|
|
37
37
|
<span class="vr"></span>
|
|
38
|
-
<%=
|
|
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-
|
|
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>
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
data/config/locales/it.yml
CHANGED
|
@@ -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
|
data/lib/lato_cms/version.rb
CHANGED
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.
|
|
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
|