lato 3.25.0 → 3.26.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.
- checksums.yaml +4 -4
- data/README.md +50 -20
- data/app/assets/javascripts/lato/controllers/lato_account_webauthn_controller.js +8 -5
- data/app/assets/javascripts/lato/controllers/lato_action_controller.js +26 -15
- data/app/assets/javascripts/lato/controllers/lato_confirm_controller.js +25 -15
- data/app/assets/javascripts/lato/controllers/lato_copyclip_controller.js +28 -10
- data/app/assets/javascripts/lato/controllers/lato_form_controller.js +1 -0
- data/app/assets/javascripts/lato/controllers/lato_index_controller.js +4 -1
- data/app/assets/stylesheets/lato/application.scss +17 -0
- data/app/controllers/concerns/lato/componentable.rb +20 -8
- data/app/controllers/concerns/lato/sessionable.rb +39 -10
- data/app/controllers/lato/account_controller.rb +10 -8
- data/app/controllers/lato/application_controller.rb +25 -1
- data/app/controllers/lato/authentication_controller.rb +74 -39
- data/app/helpers/lato/components_helper.rb +52 -17
- data/app/jobs/lato/active_storage_cleaner_job.rb +8 -7
- data/app/models/lato/invitation.rb +1 -1
- data/app/models/lato/operation.rb +6 -2
- data/app/models/lato/user.rb +39 -9
- data/app/views/lato/account/_alert-accepted-privacy-policy-version.html.erb +1 -1
- data/app/views/lato/account/_alert-accepted-terms-and-conditions-version.html.erb +1 -1
- data/app/views/lato/account/_form-authenticator.html.erb +7 -3
- data/app/views/lato/account/_form-password.html.erb +3 -2
- data/app/views/lato/account/_form-web3.html.erb +5 -4
- data/app/views/lato/account/_form-webauthn.html.erb +3 -3
- data/app/views/lato/account/_security-confirmation.html.erb +8 -5
- data/app/views/lato/account/index.html.erb +1 -1
- data/app/views/lato/authentication/_fields-registration.html.erb +8 -7
- data/app/views/lato/authentication/_form-authenticator.html.erb +2 -2
- data/app/views/lato/authentication/_form-signin.html.erb +2 -2
- data/app/views/lato/authentication/_form-verify-email.html.erb +3 -2
- data/app/views/lato/authentication/_form-web3-signin.html.erb +4 -3
- data/app/views/lato/components/_form_item_input_signature.html.erb +1 -1
- data/app/views/lato/components/_index.html.erb +27 -22
- data/app/views/lato/components/_navbar_nav_item.html.erb +1 -1
- data/app/views/lato/components/_operation.html.erb +6 -3
- data/app/views/lato/components/_page_head.html.erb +1 -1
- data/app/views/lato/components/_sidebar_nav_item.html.erb +2 -1
- data/app/views/lato/mailer/invitation/invite_mail.html.erb +1 -1
- data/app/views/lato/mailer/user/email_verification_mail.html.erb +1 -1
- data/app/views/lato/mailer/user/password_update_mail.html.erb +1 -1
- data/app/views/lato/mailer/user/signin_success_mail.html.erb +1 -1
- data/app/views/layouts/lato/_action.html.erb +2 -2
- data/app/views/layouts/lato/_aside-opener.html.erb +2 -2
- data/app/views/layouts/lato/_confirm.html.erb +6 -8
- data/app/views/layouts/lato/_feedbacks.html.erb +9 -8
- data/app/views/layouts/lato/_mailer-head_content.html.erb +1 -1
- data/app/views/layouts/lato/_network.html.erb +3 -3
- data/app/views/layouts/lato/application.html.erb +3 -2
- data/config/locales/en.yml +33 -0
- data/config/locales/fr.yml +30 -0
- data/config/locales/it.yml +30 -0
- data/config/locales/ro.yml +30 -0
- data/db/migrate/20261110000000_add_second_factor_counters_to_lato_users.rb +6 -0
- data/lib/lato/config.rb +4 -0
- data/lib/lato/engine.rb +5 -0
- data/lib/lato/version.rb +1 -1
- metadata +22 -15
|
@@ -30,7 +30,7 @@ user ||= Lato::User.new
|
|
|
30
30
|
</div>
|
|
31
31
|
<% elsif session[:web3_nonce] %>
|
|
32
32
|
<div class="alert alert-light mb-0">
|
|
33
|
-
<
|
|
33
|
+
<h3 class="alert-heading h4" role="status"><%= I18n.t('lato.connecting') %></h3>
|
|
34
34
|
<div class="progress" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
|
|
35
35
|
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
|
|
36
36
|
</div>
|
|
@@ -39,13 +39,14 @@ user ||= Lato::User.new
|
|
|
39
39
|
<span id="account_form-web3__nonce" style="display: none;"><%= session[:web3_nonce] %></span>
|
|
40
40
|
<%= form.hidden_field :web3_address, id: 'account_form-web3__input-web3_address' %>
|
|
41
41
|
<%= form.hidden_field :web3_signed_nonce, id: 'account_form-web3__input-web3_signed_nonce' %>
|
|
42
|
-
<%= lato_form_submit form, '
|
|
42
|
+
<%= lato_form_submit form, I18n.t('lato.confirm'), class: %w[btn-primary d-none], id: 'account_form-web3__submit' %>
|
|
43
43
|
|
|
44
44
|
<script>
|
|
45
45
|
(async () => {
|
|
46
|
+
// Pinned exact version: dynamic import() can not carry an SRI hash (see README security checklist).
|
|
46
47
|
const ethers = await import('https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.esm.min.js')
|
|
47
48
|
|
|
48
|
-
const nonce = document.getElementById('account_form-web3__nonce').
|
|
49
|
+
const nonce = document.getElementById('account_form-web3__nonce').textContent
|
|
49
50
|
const inputAddress = document.getElementById('account_form-web3__input-web3_address')
|
|
50
51
|
const inputSignedNonce = document.getElementById('account_form-web3__input-web3_signed_nonce')
|
|
51
52
|
const submitButton = document.getElementById('account_form-web3__submit')
|
|
@@ -72,7 +73,7 @@ user ||= Lato::User.new
|
|
|
72
73
|
</script>
|
|
73
74
|
<% else %>
|
|
74
75
|
<div class="alert alert-light mb-0">
|
|
75
|
-
<
|
|
76
|
+
<h3 class="alert-heading h4"><%= I18n.t('lato.account_web3_start_title') %></h3>
|
|
76
77
|
<p>
|
|
77
78
|
<%= raw I18n.t('lato.account_web3_start_description') %>
|
|
78
79
|
</p>
|
|
@@ -20,7 +20,7 @@ end
|
|
|
20
20
|
|
|
21
21
|
<% if user.webauthn_enabled? %>
|
|
22
22
|
<div class="alert alert-success mb-3">
|
|
23
|
-
<
|
|
23
|
+
<h3 class="alert-heading h4"><%= I18n.t('lato.account_webauthn_ready_title') %></h3>
|
|
24
24
|
<p class="mb-0">
|
|
25
25
|
<%= raw I18n.t('lato.account_webauthn_ready_description') %>
|
|
26
26
|
</p>
|
|
@@ -36,7 +36,7 @@ end
|
|
|
36
36
|
<%= form.hidden_field :webauthn_credential, data: { lato_account_webauthn_target: 'credentialInput' } %>
|
|
37
37
|
|
|
38
38
|
<div class="alert alert-light mb-3" data-lato-account-webauthn-target="status">
|
|
39
|
-
<
|
|
39
|
+
<h3 class="alert-heading h4"><%= I18n.t('lato.account_webauthn_in_progress_title') %></h3>
|
|
40
40
|
<p class="mb-0">
|
|
41
41
|
<%= raw I18n.t('lato.account_webauthn_in_progress_description') %>
|
|
42
42
|
</p>
|
|
@@ -53,7 +53,7 @@ end
|
|
|
53
53
|
</div>
|
|
54
54
|
<% else %>
|
|
55
55
|
<div class="alert alert-light mb-0">
|
|
56
|
-
<
|
|
56
|
+
<h3 class="alert-heading h4"><%= I18n.t('lato.account_webauthn_start_title') %></h3>
|
|
57
57
|
<p>
|
|
58
58
|
<%= raw I18n.t('lato.account_webauthn_start_description') %>
|
|
59
59
|
</p>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
<%# Rendered in several account forms on the same page: suffix ids so each label targets its own field. %>
|
|
2
|
+
<% password_id = form.field_id(:current_password, form.object_id) %>
|
|
3
|
+
<% code_id = form.field_id(:current_authenticator_code, form.object_id) %>
|
|
1
4
|
<div class="mb-3" data-controller="lato-account-security" data-lato-account-security-challenge-url-value="<%= lato.account_security_challenge_path %>" data-lato-account-security-webauthn-value="<%= user.webauthn_enabled? && !user.authenticator_enabled? %>">
|
|
2
|
-
<%= form.label :current_password,
|
|
3
|
-
<%= form.password_field :current_password, class: "form-control", autocomplete: "current-password" %>
|
|
5
|
+
<%= form.label :current_password, I18n.t('lato.current_password'), class: "form-label", for: password_id %>
|
|
6
|
+
<%= form.password_field :current_password, id: password_id, class: "form-control", autocomplete: "current-password" %>
|
|
4
7
|
<% if user.authenticator_enabled? %>
|
|
5
|
-
<%= form.label :current_authenticator_code,
|
|
6
|
-
<%= form.text_field :current_authenticator_code, value: nil, class: "form-control", autocomplete: "one-time-code", inputmode: "numeric" %>
|
|
8
|
+
<%= form.label :current_authenticator_code, I18n.t('lato.current_authenticator_code'), class: "form-label mt-2", for: code_id %>
|
|
9
|
+
<%= form.text_field :current_authenticator_code, id: code_id, value: nil, class: "form-control", autocomplete: "one-time-code", inputmode: "numeric" %>
|
|
7
10
|
<% elsif user.webauthn_enabled? %>
|
|
8
11
|
<%= form.hidden_field :current_webauthn_credential, value: nil, data: { lato_account_security_target: "credential" } %>
|
|
9
|
-
<p class="text-muted mt-2"
|
|
12
|
+
<p class="text-muted mt-2"><%= I18n.t('lato.security_key_confirmation_help') %></p>
|
|
10
13
|
<% end %>
|
|
11
14
|
<p class="text-danger mt-2" data-lato-account-security-target="error" role="alert"></p>
|
|
12
15
|
</div>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
</div>
|
|
61
61
|
<div class="card-body">
|
|
62
62
|
<div class="alert alert-warning">
|
|
63
|
-
<
|
|
63
|
+
<h3 class="alert-heading h4"><%= I18n.t('lato.warning') %></h3>
|
|
64
64
|
<p>
|
|
65
65
|
<%= raw I18n.t('lato.account_delete_details') %>
|
|
66
66
|
</p>
|
|
@@ -7,31 +7,32 @@ email_readonly ||= false
|
|
|
7
7
|
<div class="row">
|
|
8
8
|
<div class="col col-12 col-md-6 mb-3">
|
|
9
9
|
<%= lato_form_item_label form, :first_name %>
|
|
10
|
-
<%= lato_form_item_input_text form, :first_name, required: true %>
|
|
10
|
+
<%= lato_form_item_input_text form, :first_name, required: true, autocomplete: 'given-name' %>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
13
|
<div class="col col-12 col-md-6 mb-3">
|
|
14
14
|
<%= lato_form_item_label form, :last_name %>
|
|
15
|
-
<%= lato_form_item_input_text form, :last_name, required: true %>
|
|
15
|
+
<%= lato_form_item_input_text form, :last_name, required: true, autocomplete: 'family-name' %>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<div class="col col-12 mb-3">
|
|
19
19
|
<%= lato_form_item_label form, :email %>
|
|
20
|
-
<%= lato_form_item_input_email form, :email, required: true, readonly: email_readonly %>
|
|
20
|
+
<%= lato_form_item_input_email form, :email, required: true, readonly: email_readonly, autocomplete: 'email' %>
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<div class="col col-12 col-md-6 mb-3">
|
|
24
24
|
<%= lato_form_item_label form, :password %>
|
|
25
|
-
<%= lato_form_item_input_password form, :password, required: true %>
|
|
25
|
+
<%= lato_form_item_input_password form, :password, required: true, autocomplete: 'new-password', minlength: Lato.config.auth_password_min_length, aria: { describedby: form.field_id(:password, :help) } %>
|
|
26
|
+
<div class="form-text" id="<%= form.field_id(:password, :help) %>"><%= I18n.t('lato.password_requirements', count: Lato.config.auth_password_min_length) %></div>
|
|
26
27
|
</div>
|
|
27
28
|
|
|
28
29
|
<div class="col col-12 col-md-6 mb-3">
|
|
29
30
|
<%= lato_form_item_label form, :password_confirmation %>
|
|
30
|
-
<%= lato_form_item_input_password form, :password_confirmation, required: true %>
|
|
31
|
+
<%= lato_form_item_input_password form, :password_confirmation, required: true, autocomplete: 'new-password' %>
|
|
31
32
|
</div>
|
|
32
33
|
</div>
|
|
33
34
|
|
|
34
35
|
<div class="mb-3 text-muted" style="font-size: 14px;">
|
|
35
|
-
<%= lato_form_item_input_check form, :accepted_privacy_policy_version, I18n.t('lato.privacy_policy_checkbox', link: link_to(I18n.t('lato.privacy_policy'), Lato.config.legal_privacy_policy_url, target: '_blank')), required: true %>
|
|
36
|
-
<%= lato_form_item_input_check form, :accepted_terms_and_conditions_version, I18n.t('lato.terms_and_conditions_checkbox', link: link_to(I18n.t('lato.terms_and_conditions'), Lato.config.legal_terms_and_conditions_url, target: '_blank')), required: true %>
|
|
36
|
+
<%= lato_form_item_input_check form, :accepted_privacy_policy_version, I18n.t('lato.privacy_policy_checkbox', link: link_to(I18n.t('lato.privacy_policy'), Lato.config.legal_privacy_policy_url, target: '_blank')).html_safe, required: true %>
|
|
37
|
+
<%= lato_form_item_input_check form, :accepted_terms_and_conditions_version, I18n.t('lato.terms_and_conditions_checkbox', link: link_to(I18n.t('lato.terms_and_conditions'), Lato.config.legal_terms_and_conditions_url, target: '_blank')).html_safe, required: true %>
|
|
37
38
|
</div>
|
|
@@ -10,11 +10,11 @@ user ||= Lato::User.new
|
|
|
10
10
|
<%= lato_form_errors user, class: %w[mb-3] %>
|
|
11
11
|
|
|
12
12
|
<div class="mb-3 text-center">
|
|
13
|
-
|
|
13
|
+
<%= form.label :authenticator_code, raw(I18n.t('lato.authenticator_code_help', email: h(user.email_protected))), class: 'form-label' %>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
<div class="mb-3">
|
|
17
|
-
<%= lato_form_item_input_text form, :authenticator_code, required: true %>
|
|
17
|
+
<%= lato_form_item_input_text form, :authenticator_code, required: true, autocomplete: 'one-time-code', inputmode: 'numeric', pattern: '[0-9]{6}', maxlength: 6, autofocus: true %>
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
20
|
<div>
|
|
@@ -11,12 +11,12 @@ user ||= Lato::User.new
|
|
|
11
11
|
|
|
12
12
|
<div class="mb-3">
|
|
13
13
|
<%= lato_form_item_label form, :email %>
|
|
14
|
-
<%= lato_form_item_input_email form, :email, required: true %>
|
|
14
|
+
<%= lato_form_item_input_email form, :email, required: true, autocomplete: 'username' %>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
17
|
<div class="mb-3">
|
|
18
18
|
<%= lato_form_item_label form, :password %>
|
|
19
|
-
<%= lato_form_item_input_password form, :password, required: true %>
|
|
19
|
+
<%= lato_form_item_input_password form, :password, required: true, autocomplete: 'current-password' %>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<div>
|
|
@@ -10,13 +10,14 @@ code ||= nil
|
|
|
10
10
|
<%= form_with model: user, url: lato.authentication_verify_email_action_path(id: user.id), class: 'text-center', data: { turbo_frame: '_self' } do |form| %>
|
|
11
11
|
<% if code %>
|
|
12
12
|
<%= form.hidden_field :code, value: code %>
|
|
13
|
-
|
|
13
|
+
<%# Visible fallback when JavaScript is disabled or blocked by CSP. %>
|
|
14
|
+
<button type="submit" class="btn btn-primary"><%= I18n.t('lato.verify_email') %></button>
|
|
14
15
|
<script>document.getElementById('authentication_form_verify_email').querySelector('button[type="submit"]').click()</script>
|
|
15
16
|
<% else %>
|
|
16
17
|
<%= lato_form_notices class: %w[mb-3], fixed: true %>
|
|
17
18
|
<%= lato_form_errors user, class: %w[mb-3], fixed: true %>
|
|
18
19
|
|
|
19
|
-
<%= link_to '
|
|
20
|
+
<%= link_to I18n.t('lato.back_to_account'), lato.account_path, class: %w[btn btn-primary] %>
|
|
20
21
|
<% end %>
|
|
21
22
|
<% end %>
|
|
22
23
|
<% end %>
|
|
@@ -12,7 +12,7 @@ user ||= Lato::User.new
|
|
|
12
12
|
<% if session[:web3_nonce] %>
|
|
13
13
|
|
|
14
14
|
<div class="alert alert-light mb-0 text-center">
|
|
15
|
-
<h4 class="alert-heading"
|
|
15
|
+
<h4 class="alert-heading" role="status"><%= I18n.t('lato.connecting') %></h4>
|
|
16
16
|
<div class="progress" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
|
|
17
17
|
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
|
|
18
18
|
</div>
|
|
@@ -21,13 +21,14 @@ user ||= Lato::User.new
|
|
|
21
21
|
<span id="account_form-web3__nonce" style="display: none;"><%= session[:web3_nonce] %></span>
|
|
22
22
|
<%= form.hidden_field :web3_address, id: 'account_form-web3__input-web3_address' %>
|
|
23
23
|
<%= form.hidden_field :web3_signed_nonce, id: 'account_form-web3__input-web3_signed_nonce' %>
|
|
24
|
-
<%= lato_form_submit form, '
|
|
24
|
+
<%= lato_form_submit form, I18n.t('lato.confirm'), class: %w[btn-primary d-none], id: 'account_form-web3__submit' %>
|
|
25
25
|
|
|
26
26
|
<script>
|
|
27
27
|
(async () => {
|
|
28
|
+
// Pinned exact version: dynamic import() can not carry an SRI hash (see README security checklist).
|
|
28
29
|
const ethers = await import('https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.esm.min.js')
|
|
29
30
|
|
|
30
|
-
const nonce = document.getElementById('account_form-web3__nonce').
|
|
31
|
+
const nonce = document.getElementById('account_form-web3__nonce').textContent
|
|
31
32
|
const inputAddress = document.getElementById('account_form-web3__input-web3_address')
|
|
32
33
|
const inputSignedNonce = document.getElementById('account_form-web3__input-web3_signed_nonce')
|
|
33
34
|
const submitButton = document.getElementById('account_form-web3__submit')
|
|
@@ -102,7 +102,7 @@ default_tab = existing_url ? 'current' : 'draw'
|
|
|
102
102
|
<%# Clear button (solo nelle tab di editing) %>
|
|
103
103
|
<div class="d-flex justify-content-end mt-2" data-lato-input-signature-target="clearRow">
|
|
104
104
|
<button type="button"
|
|
105
|
-
class="btn btn-outline-
|
|
105
|
+
class="btn btn-outline-primary btn-sm"
|
|
106
106
|
style="font-size: 0.75rem;"
|
|
107
107
|
data-action="click->lato-input-signature#clearSignature">
|
|
108
108
|
<i class="bi bi-eraser"></i> <%= I18n.t('lato.signature_clear') %>
|
|
@@ -9,9 +9,10 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
|
|
|
9
9
|
|
|
10
10
|
<%= turbo_frame_tag "lato_index_#{key}_#{model_name_underscore}", class: "lato-index #{browser.device.mobile? ? 'lato-index-mob' : 'lato-index-desk'}" do %>
|
|
11
11
|
|
|
12
|
-
<%= form_tag request.path, method: :get, data: { controller: 'lato-form lato-index', turbo_frame: '_self' } do %>
|
|
12
|
+
<%= form_tag request.path, method: :get, data: { controller: 'lato-form lato-index', lato_index_key_value: key, turbo_frame: '_self' } do %>
|
|
13
13
|
<%= hidden_field_tag :key, key %>
|
|
14
|
-
<% params.each do |k, v| %>
|
|
14
|
+
<% params.except(:controller, :action, :key).each do |k, v| %>
|
|
15
|
+
<% next unless v.is_a?(String) %>
|
|
15
16
|
<%= hidden_field_tag k, v %>
|
|
16
17
|
<% end %>
|
|
17
18
|
|
|
@@ -33,7 +34,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
|
|
|
33
34
|
<% if custom_actions.any? %>
|
|
34
35
|
<div class="btn-group ms-2">
|
|
35
36
|
<% custom_actions.each do |action_key, action_params| %>
|
|
36
|
-
<%= link_to action_params[:path], { class: 'btn btn-primary' }.merge(action_params) do %>
|
|
37
|
+
<%= link_to action_params[:path], { class: 'btn btn-primary', 'aria-label': action_params[:title] }.merge(action_params) do %>
|
|
37
38
|
<i class="<%= action_params[:icon] %>"></i>
|
|
38
39
|
<% end %>
|
|
39
40
|
<% end %>
|
|
@@ -42,7 +43,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
|
|
|
42
43
|
|
|
43
44
|
<% if dropdown_actions %>
|
|
44
45
|
<div class="dropdown ms-2">
|
|
45
|
-
<%= content_tag :button, { class: 'btn btn-primary dropdown-toggle', 'data-bs-toggle': 'dropdown' }.merge(dropdown_actions) do %>
|
|
46
|
+
<%= content_tag :button, { class: 'btn btn-primary dropdown-toggle', 'data-bs-toggle': 'dropdown', 'aria-label': dropdown_actions[:title] }.merge(dropdown_actions) do %>
|
|
46
47
|
<i class="<%= dropdown_actions[:icon] %>"></i>
|
|
47
48
|
<% end %>
|
|
48
49
|
<ul class="dropdown-menu">
|
|
@@ -98,7 +99,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
|
|
|
98
99
|
<span class="text-muted"><%= collection_total %> <%= I18n.t('lato.total_results').downcase %></span>
|
|
99
100
|
<% end %>
|
|
100
101
|
<% if collection.respond_to?(:total_pages) %>
|
|
101
|
-
<div class="mt-3"><%=
|
|
102
|
+
<div class="mt-3"><%= lato_index_paginate(collection, key, total_pages: total_pages, window: 1) %></div>
|
|
102
103
|
<% end %>
|
|
103
104
|
</div>
|
|
104
105
|
|
|
@@ -109,28 +110,32 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
|
|
|
109
110
|
<thead>
|
|
110
111
|
<tr class="align-middle">
|
|
111
112
|
<% columns.each do |column| %>
|
|
112
|
-
|
|
113
|
+
<% column_label = collection_test_istance.respond_to?(column) ? collection.model.human_attribute_name(column) : lato_index_dynamic_label({
|
|
114
|
+
key: key,
|
|
115
|
+
model_name: model_name,
|
|
116
|
+
column: column
|
|
117
|
+
}) %>
|
|
118
|
+
<% sort_direction = sortable_columns.include?(column) ? { "#{column}|ASC" => 'ascending', "#{column}|DESC" => 'descending' }[params["#{key}_sort_by"]] : nil %>
|
|
119
|
+
<%= content_tag :th, scope: 'col', class: "lato-index-desk-col-label lato-index-desk-col-label-#{column}", 'aria-sort': sort_direction do %>
|
|
113
120
|
<div class="d-flex align-items-center justify-content-between">
|
|
114
|
-
<span><%=
|
|
115
|
-
key: key,
|
|
116
|
-
model_name: model_name,
|
|
117
|
-
column: column
|
|
118
|
-
}) %></span>
|
|
121
|
+
<span><%= column_label %></span>
|
|
119
122
|
<% if sortable_columns.include?(column) %>
|
|
120
123
|
<div class="btn-group ms-3 lato-index-col-sort">
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
<% [['ASC', 'ascending', 'bi-sort-up'], ['DESC', 'descending', 'bi-sort-down']].each do |direction, direction_name, icon| %>
|
|
125
|
+
<% active = params["#{key}_sort_by"] == "#{column}|#{direction}" %>
|
|
126
|
+
<button
|
|
127
|
+
type="submit"
|
|
128
|
+
name="<%= "#{key}_sort_by" %>"
|
|
129
|
+
value="<%= column %>|<%= direction %>"
|
|
130
|
+
class="btn btn-sm <%= active ? 'btn-primary' : 'btn-outline-primary' %>"
|
|
131
|
+
aria-label="<%= I18n.t("lato.sort_#{direction_name}", column: strip_tags(column_label.to_s)) %>"
|
|
132
|
+
aria-pressed="<%= active %>"
|
|
133
|
+
><i class="bi <%= icon %>" aria-hidden="true"></i></button>
|
|
134
|
+
<% end %>
|
|
130
135
|
</div>
|
|
131
136
|
<% end %>
|
|
132
137
|
</div>
|
|
133
|
-
|
|
138
|
+
<% end %>
|
|
134
139
|
<% end %>
|
|
135
140
|
</tr>
|
|
136
141
|
</thead>
|
|
@@ -156,7 +161,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
|
|
|
156
161
|
<td colspan="<%= columns.length %>">
|
|
157
162
|
<div class="d-flex justify-content-between align-items-center">
|
|
158
163
|
<% if collection.respond_to?(:total_pages) %>
|
|
159
|
-
<div><%=
|
|
164
|
+
<div><%= lato_index_paginate(collection, key, total_pages: total_pages) %></div>
|
|
160
165
|
<% else %>
|
|
161
166
|
<div></div>
|
|
162
167
|
<% end %>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
<% if operation.completed_status? %>
|
|
31
31
|
<div class="text-center">
|
|
32
|
-
<div class="fs-1 text-success">
|
|
32
|
+
<div class="fs-1 text-success-emphasis">
|
|
33
33
|
<i class="bi bi-check-circle"></i>
|
|
34
34
|
</div>
|
|
35
35
|
<h2><%= I18n.t('lato.operation_completed_title') %></h2>
|
|
@@ -40,14 +40,17 @@
|
|
|
40
40
|
<% end %>
|
|
41
41
|
|
|
42
42
|
<% if operation.output_message? %>
|
|
43
|
-
<p class="text-success fw-bold">
|
|
43
|
+
<p class="text-success-emphasis fw-bold">
|
|
44
44
|
<%= operation.output_message %>
|
|
45
45
|
</p>
|
|
46
46
|
<% end %>
|
|
47
47
|
|
|
48
48
|
<% if operation.output_action? %>
|
|
49
49
|
<div class="mt-3 d-flex justify-content-center">
|
|
50
|
-
|
|
50
|
+
<%# Only http(s) and relative links: a javascript: href built from job input would run on click. %>
|
|
51
|
+
<% output_href = operation.output_action['href'].to_s %>
|
|
52
|
+
<% output_href = '#' unless output_href.start_with?('/', 'http://', 'https://') && !output_href.start_with?('//') %>
|
|
53
|
+
<%= link_to operation.output_action['label'], output_href, { class: 'btn btn-primary' }.merge(operation.output_action['attributes'] || {}) %>
|
|
51
54
|
</div>
|
|
52
55
|
<% end %>
|
|
53
56
|
</div>
|
|
@@ -9,6 +9,7 @@ unless has_children
|
|
|
9
9
|
class: ['nav-link position-relative', (active ? 'active' : 'link-dark')].join(' '),
|
|
10
10
|
}
|
|
11
11
|
link_to_options[:target] = '_blank' if external
|
|
12
|
+
link_to_options[:aria] = { current: 'page' } if active
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
%>
|
|
@@ -28,7 +29,7 @@ end
|
|
|
28
29
|
<ul class="list-unstyled mt-2 border-start border-1 ps-2">
|
|
29
30
|
<% children.each do |child| %>
|
|
30
31
|
<li class="py-1">
|
|
31
|
-
<%= link_to child[:path], class: ['nav-link py-1', (active == child[:key] ? 'active' : 'link-dark')].join(' ') do %>
|
|
32
|
+
<%= link_to child[:path], class: ['nav-link py-1', (active == child[:key] ? 'active' : 'link-dark')].join(' '), aria: { current: ('page' if active == child[:key]) } do %>
|
|
32
33
|
<%= child[:label] %>
|
|
33
34
|
<% end %>
|
|
34
35
|
</li>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<% 10.times do %>
|
|
2
2
|
|
|
3
|
-
<div class="modal fade" data-lato-action-target="modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static"
|
|
3
|
+
<div class="modal fade" data-lato-action-target="modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
|
4
4
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" data-lato-action-target="modalDialog">
|
|
5
5
|
<div class="modal-content">
|
|
6
6
|
<div class="modal-header">
|
|
7
7
|
<h2 class="mb-0 mt-0 fs-4" data-lato-action-target="modalTitle"></h2>
|
|
8
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="
|
|
8
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= I18n.t('lato.close') %>"></button>
|
|
9
9
|
</div>
|
|
10
10
|
<div class="modal-body" data-lato-action-target="modalBody">
|
|
11
11
|
...
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
class="aside-opener shadow"
|
|
3
3
|
data-controller="lato-aside-opener"
|
|
4
4
|
data-action="click->lato-aside-opener#onClickToggle"
|
|
5
|
-
aria-label="
|
|
5
|
+
aria-label="<%= I18n.t('lato.toggle_sidebar') %>"
|
|
6
6
|
>
|
|
7
|
-
<i class="bi bi-layout-text-sidebar-reverse"></i>
|
|
7
|
+
<i class="bi bi-layout-text-sidebar-reverse" aria-hidden="true"></i>
|
|
8
8
|
</button>
|
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
<div class="lato-confirm modal fade" data-controller="lato-confirm" tabindex="-1" aria-hidden="true"
|
|
1
|
+
<div class="lato-confirm modal fade" data-controller="lato-confirm" tabindex="-1" aria-hidden="true" aria-labelledby="latoConfirmTitle">
|
|
2
2
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
|
3
3
|
<div class="modal-content">
|
|
4
4
|
<div class="modal-header">
|
|
5
|
-
<h2 class="mb-0 mt-0 fs-4"><%= I18n.t('lato.confirm_title') %></h2>
|
|
6
|
-
<button type="button" class="btn-close" data-action="click->lato-confirm#confirmCancel" aria-label="
|
|
5
|
+
<h2 class="mb-0 mt-0 fs-4" id="latoConfirmTitle"><%= I18n.t('lato.confirm_title') %></h2>
|
|
6
|
+
<button type="button" class="btn-close" data-action="click->lato-confirm#confirmCancel" aria-label="<%= I18n.t('lato.close') %>"></button>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="modal-body" data-lato-confirm-target="modalBody">
|
|
9
9
|
</div>
|
|
10
10
|
<div class="modal-footer">
|
|
11
11
|
<button
|
|
12
12
|
type="button"
|
|
13
|
-
class="btn btn-
|
|
13
|
+
class="btn btn-secondary"
|
|
14
14
|
data-action="click->lato-confirm#confirmCancel"
|
|
15
|
-
aria-label="<%= I18n.t('lato.confirm_no') %>"
|
|
16
15
|
><%= I18n.t('lato.confirm_no') %></button>
|
|
17
16
|
<button
|
|
18
17
|
type="button"
|
|
19
|
-
class="btn btn-
|
|
18
|
+
class="btn btn-primary"
|
|
20
19
|
data-action="click->lato-confirm#confirmSuccess"
|
|
21
|
-
aria-label="<%= I18n.t('lato.confirm_yes') %>"
|
|
22
20
|
><%= I18n.t('lato.confirm_yes') %></button>
|
|
23
21
|
</div>
|
|
24
22
|
</div>
|
|
25
23
|
</div>
|
|
26
|
-
</div>
|
|
24
|
+
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<div class="toast-container">
|
|
2
2
|
<% if notice %>
|
|
3
3
|
<div class="position-fixed end-0 bottom-0 p-3" style="z-index: 11;">
|
|
4
|
-
<div data-controller="lato-feedback" id="feedbackNotice" class="toast hide" role="
|
|
4
|
+
<div data-controller="lato-feedback" id="feedbackNotice" class="toast hide" role="status" aria-live="polite" aria-atomic="true">
|
|
5
5
|
<div class="toast-header">
|
|
6
|
-
<i class="bi bi-check-circle-fill text-success me-2"></i>
|
|
7
|
-
<strong class="me-auto"><%= I18n.t('lato.
|
|
8
|
-
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="
|
|
6
|
+
<i class="bi bi-check-circle-fill text-success me-2" aria-hidden="true"></i>
|
|
7
|
+
<strong class="me-auto"><%= I18n.t('lato.notice_title') %></strong>
|
|
8
|
+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="<%= I18n.t('lato.close') %>"></button>
|
|
9
9
|
</div>
|
|
10
10
|
<div class="toast-body">
|
|
11
11
|
<%= notice %>
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
<% if alert %>
|
|
18
18
|
<div class="position-fixed end-0 bottom-0 p-3" style="z-index: 11;">
|
|
19
|
-
|
|
19
|
+
<%# Errors stay visible until dismissed: auto hiding gives no time to read them. %>
|
|
20
|
+
<div data-controller="lato-feedback" id="feedbackAlert" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false">
|
|
20
21
|
<div class="toast-header">
|
|
21
|
-
<i class="bi bi-
|
|
22
|
-
<strong class="me-auto"><%= I18n.t('lato.
|
|
23
|
-
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="
|
|
22
|
+
<i class="bi bi-exclamation-triangle-fill text-danger me-2" aria-hidden="true"></i>
|
|
23
|
+
<strong class="me-auto"><%= I18n.t('lato.alert_title') %></strong>
|
|
24
|
+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="<%= I18n.t('lato.close') %>"></button>
|
|
24
25
|
</div>
|
|
25
26
|
<div class="toast-body">
|
|
26
27
|
<%= alert %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="fixed-bottom p-3 d-none" data-controller="lato-network">
|
|
2
|
-
<span class="badge rounded-pill text-bg-danger fs-6 d-none" data-lato-network-target="offline"
|
|
3
|
-
<span class="badge rounded-pill text-bg-success fs-6 d-none" data-lato-network-target="online"
|
|
1
|
+
<div class="fixed-bottom p-3 d-none" data-controller="lato-network" role="status" aria-live="polite">
|
|
2
|
+
<span class="badge rounded-pill text-bg-danger fs-6 d-none" data-lato-network-target="offline"><%= I18n.t('lato.network_offline') %></span>
|
|
3
|
+
<span class="badge rounded-pill text-bg-success fs-6 d-none" data-lato-network-target="online"><%= I18n.t('lato.network_online') %></span>
|
|
4
4
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no,interactive-widget=resizes-content">
|
|
6
|
-
<title><%= @layout_page_title %></title>
|
|
6
|
+
<title><%= @layout_page_title.presence || Lato.config.application_title %></title>
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
8
|
<%= csp_meta_tag %>
|
|
9
9
|
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
class="controller-<%= controller_name %> action-<%= action_name %> <%= @layout_body_classes.join(' ') %> <%= cookies[:lato_aside_hidden] == 'true' ? 'aside-hidden' : '' %>"
|
|
28
28
|
data-controller="lato-action lato-guide"
|
|
29
29
|
>
|
|
30
|
+
<a class="visually-hidden-focusable position-absolute top-0 start-0 p-2 bg-white" style="z-index: 2100;" href="#main"><%= I18n.t('lato.skip_to_content') %></a>
|
|
30
31
|
<header>
|
|
31
32
|
<%= render 'layouts/lato/navbar' %>
|
|
32
33
|
</header>
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
</aside>
|
|
40
41
|
<% end %>
|
|
41
42
|
|
|
42
|
-
<main class="<%= @layout_sidebar ? 'layout_sidebar' : '' %>">
|
|
43
|
+
<main id="main" tabindex="-1" class="<%= @layout_sidebar ? 'layout_sidebar' : '' %>">
|
|
43
44
|
<%= render 'layouts/lato/content' %>
|
|
44
45
|
</main>
|
|
45
46
|
</div>
|