Simplify and improve labeled form

This commit is contained in:
2026-02-20 23:58:53 +01:00
parent 675eb0aad8
commit 84945fa4b4
20 changed files with 251 additions and 206 deletions

View File

@@ -15,7 +15,13 @@
*= require_self *= require_self
*/ */
/* Strive for simplicity. Style elements only, if possible. */ /* Strive for simplicity:
* * style elements/tags only - if possible,
* * replace element/tag name with class name - if element has to be styled
* differently depending on context (e.g. form)
*
* NOTE: Style in a modular way, similar to how CSS @scope would be used,
* to make transition easier once @scope is widely available */
:root { :root {
--color-focus-gray: #f3f3f3; --color-focus-gray: #f3f3f3;
--color-border-gray: #dddddd; --color-border-gray: #dddddd;
@@ -215,7 +221,7 @@ body {
font-family: system-ui; font-family: system-ui;
margin: 0.4em; margin: 0.4em;
} }
body:not(:has(.topside)) { body:not(:has(.topside-area)) {
grid-template-areas: grid-template-areas:
"header header header" "header header header"
"nav nav nav" "nav nav nav"
@@ -250,16 +256,16 @@ header {
fill: var(--color-blue); fill: var(--color-blue);
} }
.topside { .topside-area {
grid-area: topside; grid-area: topside;
} }
.leftside { .leftside-area {
grid-area: leftside; grid-area: leftside;
} }
.main { .main-area {
grid-area: main; grid-area: main;
} }
.rightside { .rightside-area {
grid-area: rightside; grid-area: rightside;
} }
@@ -270,7 +276,7 @@ header {
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
grid-template-rows: max-content; grid-template-rows: max-content;
} }
.tools { .tools-area {
grid-area: tools; grid-area: tools;
} }
@@ -334,46 +340,43 @@ header {
opacity: 1; opacity: 1;
} }
/* TODO: Hover over invalid should work like in measurements (thin vs thick border) */
/* TODO: Update form styling: simplify selectors, deduplicate, remove non-font rem. */ .labeled-form {
form table { align-items: center;
border-spacing: 0.8rem; display: grid;
gap: 0.9em 1.1em;
grid-template-columns: 1fr minmax(max-content, 0.5fr) 1fr;
} }
form tr td:first-child { .labeled-form label {
color: var(--color-gray); color: var(--color-gray);
font-size: 0.9rem; font-size: 0.9rem;
padding-right: 0.25rem; grid-column: 1;
text-align: right; text-align: right;
white-space: nowrap;
} }
form label.required { .labeled-form label.required {
font-weight: bold; font-weight: bold;
} }
form label.error, /* Don't style `label.error + input` if case already covered by input:invalid */
form td.error::after { .labeled-form label.error {
color: var(--color-red); color: var(--color-red);
} }
form td.error { .labeled-form em {
display: -webkit-box;
}
form td.error::after {
content: attr(data-content);
font-size: 0.9rem;
margin-left: 1rem;
padding: 0.25rem 0;
position: absolute;
}
form em {
color: var(--color-text-gray); color: var(--color-text-gray);
font-size: 0.75rem; font-size: 0.75rem;
font-weight: normal;
} }
form input[type=submit] { .labeled-form input {
grid-column: 2;
}
.labeled-form input[type=submit] {
font-size: 1rem; font-size: 1rem;
margin: 1.5rem auto 0 auto; margin: 1.5em auto 0 auto;
padding: 0.75rem; padding: 0.75em;
} }
/* TODO: remove .items class and make 'form table' work properly */ /* TODO: remove .items class (?) and make 'form table' work properly */
table.items { table.items {
border-spacing: 0; border-spacing: 0;
border: solid 1px var(--color-border-gray); border: solid 1px var(--color-border-gray);
@@ -588,9 +591,6 @@ form table.items td:first-child {
fill: var(--color-border-gray) !important; fill: var(--color-border-gray) !important;
pointer-events: none; pointer-events: none;
} }
.unwrappable {
white-space: nowrap;
}
details { details {

View File

@@ -1,6 +1,11 @@
class RegistrationsController < Devise::RegistrationsController class RegistrationsController < Devise::RegistrationsController
before_action :authenticate_user!, only: [:edit, :update, :destroy] before_action :authenticate_user!, only: [:edit, :update, :destroy]
def destroy
# TODO: Disallow/disable deletion for last admin account; update :edit view
super
end
protected protected
def update_resource(resource, params) def update_resource(resource, params)

View File

@@ -1,59 +1,84 @@
module ApplicationHelper module ApplicationHelper
# TODO: replace legacy content_tag with tag.tagname class LabeledFormBuilder < ActionView::Helpers::FormBuilder
class LabelledFormBuilder < ActionView::Helpers::FormBuilder (field_helpers - [:label, :hidden_field]).each do |selector|
(field_helpers - [:label]).each do |selector|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{selector}(method, options = {}) def #{selector}(method, options = {})
labelled_row_for(method, options) { super } labeled_field_for(method, options) { super }
end end
RUBY_EVAL RUBY_EVAL
end end
def select(method, choices = nil, options = {}, html_options = {}) def select(method, choices = nil, options = {}, html_options = {})
labelled_row_for(method, options) { super } labeled_field_for(method, options) { super }
end
def submit(value, options = {})
@template.content_tag :tr do
@template.content_tag :td, super, colspan: 2
end
end
def form_for(&block)
@template.content_tag(:table, class: "centered") { yield(self) } +
# Display leftover error messages (there shouldn't be any)
@template.content_tag(:div, @object&.errors.full_messages.join(@template.tag(:br)))
end end
private private
def labelled_row_for(method, options) def labeled_field_for(method, options)
@template.content_tag :tr do field = if options.delete(:readonly) then
@template.content_tag(:td, label_for(method, options), class: "unwrappable") + value = object.public_send(method)
@template.content_tag(:td, options.delete(:readonly) ? @object.public_send(method) : yield, value = @template.l(value) if value.respond_to?(:strftime)
@object&.errors[method].present? ? value ||= options[:placeholder]
{class: "error", data: {content: @object&.errors.delete(method).join(" and ")}} : else
{}) yield
end end
label_for(method, options) + field
end end
def label_for(method, options = {}) def label_for(method, options = {})
return '' if options[:label] == false
text = options.delete(:label)
text ||= @object.class.human_attribute_name(method).capitalize
classes = @template.class_names(required: options[:required], classes = @template.class_names(required: options[:required],
error: @object&.errors[method].present?) error: object.errors[method].present?)
label = label(method, "#{text}:", class: classes)
hint = options.delete(:hint)
label + (@template.tag(:br) + @template.content_tag(:em, hint) if hint) handler = {missing_interpolation_argument_handler: method(:interpolation_missing)}
# Label translation search order:
# controller.action.* => helpers.label.model.* => activerecord.attributes.model.*
# First 2 levels are translated recursively.
label(method, class: classes) do |builder|
translation = I18n.config.with(**handler) { deep_translate(method, **options) }
translation.presence || "#{builder.translation}:"
end end
end end
def labelled_form_for(record, options = {}, &block) def interpolation_missing(key, values, string)
options = options.deep_merge(builder: LabelledFormBuilder, data: {turbo: false}) @template.instance_values[key.to_s] || deep_translate(key, **values)
form_for(record, **options) { |f| f.form_for(&block) } end
# Extension to label text translation:
# * recursive translation,
# * interpolation of (_relative_) translation key names and instance variables,
# * pluralization based on any interpolable value
# TODO: add unit tests for the above
def deep_translate(key, **options)
options[:default] = [
:".#{key}",
:"helpers.label.#{@object_name}.#{key}_html",
:"helpers.label.#{@object_name}.#{key}",
""
]
# At least 1 interpolation key is required for #translate to act on
# missing interpolation arguments (i.e. call custom handler).
# Also setting `key` to nil avoids recurrent translation.
options[key] = nil
@template.t(".#{key}_html", **options) do |translation, resolved_key|
if translation.is_a?(Array) && (count = translation.to_h[:count])
@template.t(resolved_key, count: I18n.interpolate(count, **options), **options)
else
translation
end
end
end
end
def labeled_form_for(record, options = {}, &block)
extra_options = {builder: LabeledFormBuilder,
data: {turbo: false},
html: {class: 'labeled-form'}}
options = options.deep_merge(extra_options) do |key, left, right|
key == :class ? class_names(left, right) : right
end
form_for(record, **options, &block)
end end
class TabularFormBuilder < ActionView::Helpers::FormBuilder class TabularFormBuilder < ActionView::Helpers::FormBuilder
@@ -111,6 +136,7 @@ module ApplicationHelper
# and the first input gets focus. # and the first input gets focus.
record_object, options = nil, record_object if record_object.is_a?(Hash) record_object, options = nil, record_object if record_object.is_a?(Hash)
options.merge!(builder: TabularFormBuilder, skip_default_ids: true) options.merge!(builder: TabularFormBuilder, skip_default_ids: true)
# TODO: set error message with setCustomValidity instead of rendering to flash?
render_errors(record_object || record_name) render_errors(record_object || record_name)
fields_for(record_name, record_object, **options, &block) fields_for(record_name, record_object, **options, &block)
end end
@@ -122,7 +148,7 @@ module ApplicationHelper
end end
def svg_tag(source, label = nil, options = {}) def svg_tag(source, label = nil, options = {})
svg_tag = content_tag :svg, options do svg_tag = tag.svg(options) do
tag.use(href: "#{image_path(source + ".svg")}#icon") tag.use(href: "#{image_path(source + ".svg")}#icon")
end end
label.blank? ? svg_tag : svg_tag + tag.span(label) label.blank? ? svg_tag : svg_tag + tag.span(label)
@@ -170,17 +196,23 @@ module ApplicationHelper
def image_link_to_unless_current(name, image = nil, options = nil, html_options = {}) def image_link_to_unless_current(name, image = nil, options = nil, html_options = {})
name, html_options = link_or_button_options(:link, name, image, html_options) name, html_options = link_or_button_options(:link, name, image, html_options)
html_options = html_options.deep_merge DISABLED_ATTRIBUTES if current_page?(options) # NOTE: Starting from Rails 8.1.0, below condition can be replaced with:
# current_page?(options, method: [:get, :post])
if request.path == url_for(options)
html_options = html_options.deep_merge DISABLED_ATTRIBUTES
end
link_to name, options, html_options link_to name, options, html_options
end end
def render_errors(records) def render_errors(records)
flash[:alert] ||= [] # Conversion of flash to Array only required because of Devise
flash[:alert] = Array(flash[:alert])
Array(records).each { |record| flash[:alert] += record.errors.full_messages } Array(records).each { |record| flash[:alert] += record.errors.full_messages }
end end
def render_flash_messages def render_flash_messages
flash.map do |entry, messages| flash.map do |entry, messages|
# Conversion of flash to Array only required because of Devise
Array(messages).map do |message| Array(messages).map do |message|
tag.div class: "flash #{entry}" do tag.div class: "flash #{entry}" do
tag.div(sanitize(message)) + tag.button(sanitize("&times;"), tabindex: -1, tag.div(sanitize(message)) + tag.button(sanitize("&times;"), tabindex: -1,
@@ -203,8 +235,6 @@ module ApplicationHelper
private private
def link_or_button_options(type, name, image = nil, html_options) def link_or_button_options(type, name, image = nil, html_options)
name = svg_tag("pictograms/#{image}", name) if image
html_options[:class] = class_names( html_options[:class] = class_names(
html_options[:class], html_options[:class],
'button', 'button',
@@ -215,9 +245,10 @@ module ApplicationHelper
html_options[:onclick] = "return confirm('\#{html_options[:onclick][:confirm]}');" html_options[:onclick] = "return confirm('\#{html_options[:onclick][:confirm]}');"
end end
if type == :link && !(html_options[:onclick] || html_options.dig(:data, :turbo_stream)) link_is_local = html_options[:onclick] || html_options.dig(:data, :turbo_stream)
name += '...' name = name.to_s
end name += '...' if type == :link && !link_is_local
name = svg_tag("pictograms/#{image}", name) if image
[name, html_options] [name, html_options]
end end

View File

@@ -1,16 +1,17 @@
<div class="rightside buttongrid"> <div class="rightside-area buttongrid">
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%# TODO: implement Import all %> <%# TODO: implement Import all %>
<%#= image_button_to t('.import_all'), 'download-multiple-outline', <%#= image_button_to t('.import_all'), 'download-multiple-outline',
import_all_default_units_path, data: {turbo_stream: true} %> import_all_default_units_path, data: {turbo_stream: true} %>
<% end %> <% end %>
<%= image_link_to t('.back'), 'arrow-left-bold-outline', units_path, class: 'tools' %> <%= image_link_to t('.back'), 'arrow-left-bold-outline', units_path,
class: 'tools-area' %>
</div> </div>
<table class="main items"> <table class="main-area items">
<thead> <thead>
<tr> <tr>
<th><%= User.human_attribute_name(:symbol).capitalize %></th> <th><%= Unit.human_attribute_name(:symbol) %></th>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<th><%= t '.actions' %></th> <th><%= t '.actions' %></th>
<% end %> <% end %>

View File

@@ -47,7 +47,7 @@
<%= render_flash_messages %> <%= render_flash_messages %>
</div> </div>
<%# Allow overwriting/clearing navigation menu for some views %> <%# Allows overwriting/clearing navigation menu for some views %>
<nav class="navigation"> <nav class="navigation">
<%= content_for(:navigation) || (navigation_menu if user_signed_in?) %> <%= content_for(:navigation) || (navigation_menu if user_signed_in?) %>
</nav> </nav>

View File

@@ -1,5 +1,5 @@
<%= tabular_form_with model: Measurement.new, id: :measurement_form, <%= tabular_form_with model: Measurement.new, id: :measurement_form,
class: 'topside vflex', html: {onkeydown: 'formProcessKey(event)'} do |form| %> class: 'topside-area vflex', html: {onkeydown: 'formProcessKey(event)'} do |form| %>
<table class="items centered"> <table class="items centered">
<tbody id="readouts"></tbody> <tbody id="readouts"></tbody>
</table> </table>

View File

@@ -1,5 +1,5 @@
<%# TODO: show hint when no quantities/units defined %> <%# TODO: show hint when no quantities/units defined %>
<div class="rightside buttongrid"> <div class="rightside-area buttongrid">
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%= image_link_to t('.new_measurement'), 'plus-outline', new_measurement_path, <%= image_link_to t('.new_measurement'), 'plus-outline', new_measurement_path,
id: :new_measurement_link, onclick: 'this.blur();', id: :new_measurement_link, onclick: 'this.blur();',
@@ -7,7 +7,7 @@
<% end %> <% end %>
</div> </div>
<table class="main"> <table class="main-area">
<tbody id="measurements"> <tbody id="measurements">
<%= render(@measurements) || render_no_items %> <%= render(@measurements) || render_no_items %>
</tbody> </tbody>

View File

@@ -1,20 +1,20 @@
<div class="rightside buttongrid"> <div class="rightside-area buttongrid">
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%= image_link_to t('.new_quantity'), 'plus-outline', new_quantity_path, <%= image_link_to t('.new_quantity'), 'plus-outline', new_quantity_path,
id: dom_id(Quantity, :new, :link), onclick: 'this.blur();', id: dom_id(Quantity, :new, :link), onclick: 'this.blur();',
data: {turbo_stream: true} %> data: {turbo_stream: true} %>
<% end %> <% end %>
<%#= image_link_to t('.import_quantities'), 'download-outline', default_quantities_path, <%#= image_link_to t('.import_quantities'), 'download-outline', default_quantities_path,
class: 'tools' %> class: 'tools-area' %>
</div> </div>
<%= tag.div class: 'main', id: :quantity_form %> <%= tag.div class: 'main-area', id: :quantity_form %>
<table class="main items"> <table class="main-area items">
<thead> <thead>
<tr> <tr>
<th><%= Quantity.human_attribute_name(:name).capitalize %></th> <th><%= Quantity.human_attribute_name(:name) %></th>
<th><%= Quantity.human_attribute_name(:description).capitalize %></th> <th><%= Quantity.human_attribute_name(:description) %></th>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<th><%= t :actions %></th> <th><%= t :actions %></th>
<th></th> <th></th>

View File

@@ -1,19 +1,20 @@
<div class="rightside buttongrid"> <div class="rightside-area buttongrid">
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%= image_link_to t('.new_unit'), 'plus-outline', new_unit_path, <%= image_link_to t('.new_unit'), 'plus-outline', new_unit_path,
id: dom_id(Unit, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %> id: dom_id(Unit, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %>
<% end %> <% end %>
<%= image_link_to t('.import_units'), 'download-outline', default_units_path, class: 'tools' %> <%= image_link_to t('.import_units'), 'download-outline', default_units_path,
class: 'tools-area' %>
</div> </div>
<%= tag.div id: :unit_form %> <%= tag.div id: :unit_form %>
<table class="main items"> <table class="main-area items">
<thead> <thead>
<tr> <tr>
<th><%= User.human_attribute_name(:symbol).capitalize %></th> <th><%= Unit.human_attribute_name(:symbol) %></th>
<th><%= User.human_attribute_name(:description).capitalize %></th> <th><%= Unit.human_attribute_name(:description) %></th>
<th><%= User.human_attribute_name(:multiplier).capitalize %></th> <th><%= Unit.human_attribute_name(:multiplier) %></th>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<th><%= t :actions %></th> <th><%= t :actions %></th>
<th></th> <th></th>

View File

@@ -1,8 +1,9 @@
<div class="main"> <%= labeled_form_for resource, url: user_confirmation_path,
<%= labelled_form_for resource, url: user_confirmation_path do |f| %> html: {class: 'main-area'} do |f| %>
<%= f.email_field :email, required: true, size: 30, autofocus: true, autocomplete: "email",
value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> <%= f.email_field :email, required: true, size: 30, autofocus: true,
autocomplete: 'email', value:
resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email %>
<%= f.submit t(:resend_confirmation) %> <%= f.submit t(:resend_confirmation) %>
<% end %> <% end %>
</div>

View File

@@ -1,12 +1,10 @@
<table class="main items" id="users"> <table class="main-area items" id="users">
<thead> <thead>
<tr> <tr>
<th><%= User.human_attribute_name(:email).capitalize %></th> <th><%= User.human_attribute_name(:email) %></th>
<th><%= User.human_attribute_name(:status).capitalize %></th> <th><%= User.human_attribute_name(:status) %></th>
<th><%= User.human_attribute_name(:confirmed_at).capitalize %></th> <th><%= User.human_attribute_name(:confirmed_at) %></th>
<th> <th><%= User.human_attribute_name(:created_at) %>&nbsp;<sup>(UTC)</sup></th>
<%= User.human_attribute_name(:created_at).capitalize %>&nbsp;<sup>UTC</sup>
</th>
<th><%= t :actions %></th> <th><%= t :actions %></th>
</tr> </tr>
</thead> </thead>
@@ -19,18 +17,18 @@
<%= user.status %> <%= user.status %>
<% else %> <% else %>
<%= form_for user do |u| %> <%= form_for user do |u| %>
<%= u.select :status, User.statuses.keys, {}, autocomplete: "off", <%= u.select :status, User.statuses.keys, {}, autocomplete: 'off',
onchange: "this.form.requestSubmit();" %> onchange: 'this.form.requestSubmit();' %>
<% end %> <% end %>
<% end %> <% end %>
</td> </td>
<td class="svg"> <td class="svg">
<%= svg_tag "pictograms/checkbox-marked-outline" if user.confirmed_at.present? %> <%= svg_tag 'pictograms/checkbox-marked-outline' if user.confirmed_at.present? %>
</td> </td>
<td><%= user.created_at.to_fs(:db_without_sec) %></td> <td><%= l user.created_at, format: :without_tz %></td>
<td class="actions"> <td class="actions">
<% if allow_disguise?(user) %> <% if allow_disguise?(user) %>
<%= image_link_to t(".disguise"), "incognito", disguise_user_path(user) %> <%= image_link_to t('.disguise'), 'incognito', disguise_user_path(user) %>
<% end %> <% end %>
</td> </td>
</tr> </tr>

View File

@@ -1,5 +1,5 @@
<p>Welcome <%= @email %>!</p> <p>Welcome <%= @email %>!</p>
<p>You can confirm your account email through the link below:</p> <p>You can confirm your account email through the link below:</p>
<!-- FIXME: is confirmation_url valid route prefix? -->
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p> <p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>

View File

@@ -1,13 +1,12 @@
<div class="main"> <%= labeled_form_for resource, url: user_password_path,
<%= labelled_form_for resource, url: user_password_path, html: {method: :put} do |f| %> html: {method: :put, class: 'main-area'} do |f| %>
<%= f.hidden_field :reset_password_token, label: false %>
<%= f.password_field :password, label: t(".new_password"), required: true, size: 30, <%= f.hidden_field :reset_password_token %>
minlength: @minimum_password_length, autofocus: true, autocomplete: "new-password",
hint: t("users.minimum_password_length", count: @minimum_password_length) %>
<%= f.password_field :password_confirmation, label: t(".password_confirmation"),
required: true, size: 30, minlength: @minimum_password_length, autocomplete: "off" %>
<%= f.submit t(".update_password") %> <%= f.password_field :password, required: true, size: 30, autofocus: true,
minlength: @minimum_password_length, autocomplete: 'new-password' %>
<%= f.password_field :password_confirmation, required: true, size: 30,
minlength: @minimum_password_length, autocomplete: 'off' %>
<%= f.submit t('.update_password') %>
<% end %> <% end %>
</div>

View File

@@ -1,7 +1,8 @@
<div class="main"> <%= labeled_form_for resource, url: user_password_path,
<%= labelled_form_for resource, url: user_password_path do |f| %> html: {class: 'main-area'} do |f| %>
<%= f.email_field :email, required: true, size: 30, autofocus: true, autocomplete: "email" %>
<%= f.email_field :email, required: true, size: 30, autofocus: true,
autocomplete: 'email' %>
<%= f.submit t(:recover_password) %> <%= f.submit t(:recover_password) %>
<% end %> <% end %>
</div>

View File

@@ -1,31 +1,30 @@
<% content_for :navigation, flush: true do %> <% content_for :navigation, flush: true do %>
<%= link_to svg_tag("pictograms/arrow-left-bold-outline", t(:back)), <%= link_to svg_tag('pictograms/arrow-left-bold-outline', t(:back)),
request.referer.present? ? :back : root_path, class: 'tab' %> request.referer.present? ? :back : root_path, class: 'tab' %>
<% end %> <% end %>
<div class="rightside buttongrid"> <div class="rightside-area buttongrid">
<%= image_button_to t(".delete"), "account-remove-outline", user_registration_path, <%#= TODO: Disallow/disable deletion for last admin account, image_button_to_if %>
form_class: 'tools', method: :delete, data: {turbo: false}, <%= image_button_to t('.delete'), 'account-remove-outline', user_registration_path,
onclick: {confirm: t(".confirm_delete")} %> form_class: 'tools-area', method: :delete, data: {turbo: false},
onclick: {confirm: t('.confirm_delete')} %>
</div> </div>
<%= labelled_form_for resource, url: registration_path(resource), <%= labeled_form_for resource, url: registration_path(resource),
html: {method: :patch, class: 'main'} do |f| %> html: {method: :patch, class: 'main-area'} do |f| %>
<%= f.email_field :email, size: 30, autofocus: true, autocomplete: "off" %>
<% if f.object.pending_reconfirmation? %> <%= f.email_field :email, size: 30, autofocus: true, autocomplete: 'off' %>
<%= f.text_field :unconfirmed_email, readonly: true, tabindex: -1, <% if resource.pending_reconfirmation? %>
hint: t(".unconfirmed_email_hint", <%= f.text_field :unconfirmed_email, readonly: true,
timestamp: f.object.confirmation_sent_at.to_fs(:db_without_sec)) %> confirmation_sent_at: l(resource.confirmation_sent_at) %>
<% end %> <% end %>
<%= f.select :status, User.statuses, readonly: true %> <%= f.select :status, User.statuses, readonly: true %>
<%= f.password_field :password, label: t(".new_password"), size: 30, <%= f.password_field :password, size: 30, autocomplete: 'new-password',
minlength: @minimum_password_length, autocomplete: "new-password", minlength: @minimum_password_length %>
hint: t(".blank_password_hint_html", <%= f.password_field :password_confirmation, size: 30, autocomplete: 'off',
subhint: t("users.minimum_password_length", count: @minimum_password_length)) %> minlength: @minimum_password_length %>
<%= f.password_field :password_confirmation, label: t(".password_confirmation"),
size: 30, minlength: @minimum_password_length, autocomplete: "off" %>
<%= f.submit t(".update") %> <%= f.submit %>
<% end %> <% end %>

View File

@@ -1,16 +1,16 @@
<div class="main"> <div class="main-area">
<%= labelled_form_for resource, url: user_registration_path do |f| %> <%= labeled_form_for resource, url: user_registration_path do |f| %>
<%= f.email_field :email, required: true, size: 30, autofocus: true, autocomplete: "email" %> <%= f.email_field :email, required: true, size: 30, autofocus: true,
autocomplete: 'email' %>
<%= f.password_field :password, required: true, size: 30, <%= f.password_field :password, required: true, size: 30,
minlength: @minimum_password_length, autocomplete: "new-password", minlength: @minimum_password_length, autocomplete: 'new-password' %>
hint: t("users.minimum_password_length", count: @minimum_password_length) %> <%= f.password_field :password_confirmation, required: true, size: 30,
<%= f.password_field :password_confirmation, label: t(".password_confirmation"), minlength: @minimum_password_length, autocomplete: 'off' %>
required: true, size: 30, minlength: @minimum_password_length, autocomplete: "off" %>
<%= f.submit t(:register) %> <%= f.submit t(:register) %>
<% end %> <% end %>
<%= content_tag :p, t(:or), style: "text-align: center;" %> <%= content_tag :p, t(:or), style: 'text-align: center;' %>
<%= image_link_to t(:resend_confirmation), "email-sync-outline", new_user_confirmation_path, <%= image_link_to t(:resend_confirmation), 'email-sync-outline',
class: "centered" %> new_user_confirmation_path, class: 'centered' %>
</div> </div>

View File

@@ -1,17 +1,18 @@
<div class="main"> <div class="main-area">
<%= labelled_form_for resource, url: user_session_path do |f| %> <%= labeled_form_for resource, url: user_session_path do |f| %>
<%= f.email_field :email, required: true, size: 30, autofocus: true, autocomplete: "email" %> <%= f.email_field :email, required: true, size: 30, autofocus: true,
<%= f.password_field :password, required: true, size: 30, minlength: @minimum_password_length, autocomplete: 'email' %>
autocomplete: "current-password" %> <%= f.password_field :password, required: true, size: 30,
minlength: @minimum_password_length, autocomplete: 'current-password' %>
<% if devise_mapping.rememberable? %> <% if devise_mapping.rememberable? %>
<%= f.check_box :remember_me, label: t(".remember_me") %> <%= f.check_box :remember_me %>
<% end %> <% end %>
<%= f.submit t(:sign_in) %> <%= f.submit t(:sign_in) %>
<% end %> <% end %>
<%= content_tag :p, t(:or), style: "text-align: center;" %> <%= content_tag :p, t(:or), style: 'text-align: center;' %>
<%= image_link_to t(:recover_password), 'lock-reset', new_user_password_path, <%= image_link_to t(:recover_password), 'lock-reset', new_user_password_path,
class: 'centered' %> class: 'centered' %>
</div> </div>

View File

@@ -1,19 +1,18 @@
<% content_for :navigation, flush: true do %> <% content_for :navigation, flush: true do %>
<div class="left"> <%= link_to svg_tag('pictograms/arrow-left-bold-outline', t(:back)), users_path,
<%= image_link_to t(:back), "arrow-left-bold-outline", users_path %> class: 'tab' %>
</div>
<% end %> <% end %>
<%= labelled_form_for @user do |f| %> <%= labeled_form_for @user, html: {class: 'main-area'} do |f| %>
<%= f.email_field :email, readonly: true %> <%= f.email_field :email, readonly: true %>
<% if f.object.pending_reconfirmation? %> <% if @user.pending_reconfirmation? %>
<%= f.email_field :unconfirmed_email, readonly: true, <%= f.email_field :unconfirmed_email, readonly: true,
hint: t("users.registrations.edit.unconfirmed_email_hint", confirmation_sent_at: l(@user.confirmation_sent_at) %>
timestamp: f.object.confirmation_sent_at.to_fs(:db_without_sec)) %>
<% end %> <% end %>
<%# TODO: allow status change here? %>
<%= f.select :status, User.statuses, readonly: true %> <%= f.select :status, User.statuses, readonly: true %>
<%= f.text_field :created_at, readonly: true %> <%= f.text_field :created_at, readonly: true %>
<%= f.text_field :confirmed_at, readonly: true %> <%= f.text_field :confirmed_at, readonly: true, placeholder: t(:no) %>
<% end %> <% end %>

View File

@@ -1,3 +0,0 @@
# Format contains non-breaking space:
# 160.chr(Encoding::UTF_8)
Time::DATE_FORMATS[:db_without_sec] = "%Y-%m-%d %H:%M"

View File

@@ -1,22 +1,28 @@
en: en:
time:
formats:
# Format contains non-breaking space: 160.chr(Encoding::UTF_8)
default: "%Y-%m-%d %H:%M %Z"
without_tz: "%Y-%m-%d %H:%M"
errors: errors:
messages: messages:
precision_exceeded: must not exceed %{value} significant digits precision_exceeded: must not exceed %{value} significant digits
scale_exceeded: must not exceed %{value} decimal digits scale_exceeded: must not exceed %{value} decimal digits
activerecord: activerecord:
attributes: attributes:
unit: quantity:
symbol: Symbol description: Description
name: Name name: Name
multiplier: Multiplier unit:
base: Base unit base: Base unit
description: Description
multiplier: Multiplier
symbol: Symbol
user: user:
email: e-mail confirmed_at: Confirmed
status: status created_at: Registered
password: password email: E-mail
created_at: registered status: Status
confirmed_at: confirmed
unconfirmed_email: Awaiting confirmation for
errors: errors:
models: models:
unit: unit:
@@ -53,9 +59,22 @@ en:
The request is semantically incorrect and was rejected (422 Unprocessable Entity). The request is semantically incorrect and was rejected (422 Unprocessable Entity).
This should not happen, please notify site administrator. This should not happen, please notify site administrator.
helpers: helpers:
label:
user:
password_confirmation: 'Retype new password:'
password_length_hint_html:
count: '%{minimum_password_length}'
zero:
one: <br><em>(%{count} character minimum)</em>
other: <br><em>(%{count} characters minimum)</em>
remember_me: 'Remember me:'
unconfirmed_email_html: >
Awaiting confirmation for:<br><em>(since %{confirmation_sent_at})</em>
submit: submit:
create: Create create: Create
update: Update update: Update
user:
update: Update profile
layouts: layouts:
application: application:
issue_tracker: Report issue issue_tracker: Report issue
@@ -129,34 +148,27 @@ en:
disguise: View as disguise: View as
passwords: passwords:
edit: edit:
new_password: New password password_html: 'New password:%{password_length_hint_html}'
password_confirmation: Retype new password
update_password: Update password update_password: Update password
registrations: registrations:
new: new:
password_confirmation: Retype password password_html: 'Password:%{password_length_hint_html}'
password_confirmation: 'Retype password:'
edit: edit:
confirm_delete: Are you sure you want to delete profile? confirm_delete: Are you sure you want to delete profile?
All data will be irretrievably lost. All data will be irretrievably lost.
delete: Delete profile delete: Delete profile
unconfirmed_email_hint: (since %{timestamp}) password_html: >
new_password: New password New password:
password_confirmation: Retype new password <br><em>leave blank to keep unchanged</em>
blank_password_hint_html: leave blank to keep unchanged<br>%{subhint} %{password_length_hint_html}
update: Update profile
sessions:
new:
remember_me: Remember me
minimum_password_length:
zero:
one: (%{count} character minimum)
other: (%{count} characters minimum)
actions: Actions actions: Actions
add: Add add: Add
apply: Apply apply: Apply
back: Back back: Back
cancel: Cancel cancel: Cancel
delete: Delete delete: Delete
:no: 'no'
or: or or: or
register: Register register: Register
sign_in: Sign in sign_in: Sign in