forked from fixin.me/fixin.me
Simplify and improve labeled form
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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("×"), tabindex: -1,
|
tag.div(sanitize(message)) + tag.button(sanitize("×"), 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
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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) %> <sup>(UTC)</sup></th>
|
||||||
<%= User.human_attribute_name(:created_at).capitalize %> <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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user