Compare commits

...

33 Commits

Author SHA1 Message Date
cryptogopher f04c8d85be Fix SQLite query compatibility inside WITH clause
Rename model methods to #succeeding.
2026-10-18 02:34:57 +02:00
cryptogopher 9c51887024 Improve index tables
Stretch inputs to column width in place of fixed sizing.
Set minimum column widths for columns with form inputs.
Simplify table operations by removing `thead` and adding sentinel.
Manage empty table display with CSS only.
2026-10-17 19:25:46 +02:00
cryptogopher 2c845f3528 Replace patched #dom_id with #dom_target 2026-10-17 05:06:09 +02:00
cryptogopher f1578fad75 Update and move turbo_stream actions to separate file
Use CSS `visibility` instead of `display` for #show/#hide.
Access data attributes through `dataset` property.
2026-10-17 03:14:35 +02:00
cryptogopher 4d86652247 bundle update 2026-10-04 01:26:16 +02:00
cryptogopher eae0ee9d20 Improve controllers and views 2026-07-28 00:46:33 +02:00
cryptogopher eb2797bd90 Display form errors using custom validity messages 2026-07-28 00:45:08 +02:00
cryptogopher 5d051de666 Check for out of range float values
Remove unsupported attribute `[maxlength]` from `input[number]`.
2026-07-20 21:45:33 +02:00
cryptogopher ef8214cfa7 Improve tests, extend fixtures 2026-07-20 16:38:05 +02:00
cryptogopher 30c80e6e2e Fix js code and comment formatting 2026-07-12 17:20:00 +02:00
cryptogopher 159b4b340f Test Unit using float multiplier
Improve string randomization. Use Capybara table selectors.
2026-07-12 17:20:00 +02:00
cryptogopher 774b1eb19b Use [disabled] attribute only on tags that support it
Add Capybara selector for disabled links.
2026-07-12 17:20:00 +02:00
cryptogopher d4be84875e Use <pre> tag for formatting 2026-07-12 17:19:50 +02:00
cryptogopher 65e5643b13 Remove pictograms directory 2026-07-12 17:17:59 +02:00
cryptogopher b05523dcd6 Change Unit.multiplier type from decimal to float
Remove remnants related to BigDecimal.
2026-07-12 17:16:43 +02:00
cryptogopher 1ab925f6fa Improve Units tests and remove test parallelization
Parallelization produces errors (#92)
2026-05-15 00:21:10 +02:00
cryptogopher 83168092f1 Fix SQLite query compatibility inside WITH clause
SQLite3::SQLException: circular reference
2026-05-15 00:21:01 +02:00
cryptogopher 852e6e7cea Fix test passing blank input into required field 2026-05-15 00:20:28 +02:00
cryptogopher 0481b0f6f1 Allow Capybara test server IP in HostAuthorization 2026-05-07 20:53:18 +02:00
cryptogopher 76b95f7c30 Set default :text type column limit 2026-05-07 18:20:06 +02:00
cryptogopher ca7edb99e1 Add Note 2026-05-07 18:17:30 +02:00
cryptogopher 98044fc04a Remove empty test files 2026-05-05 23:41:08 +02:00
cryptogopher d3a34233b9 Set constraints on Readouts.user_id foreign key
Add Measurements table
2026-05-05 20:36:57 +02:00
cryptogopher 4a36ffc8bf Move migration defaults to ActiveRecord migration strategy
Remove redundant defaults
2026-05-05 20:36:51 +02:00
cryptogopher 78639b4c1e Upgrade Rails to 8.1.3 2026-05-05 20:35:00 +02:00
cryptogopher c4def9cd86 Filter :email field logging only in production env 2026-04-30 18:26:43 +02:00
cryptogopher 3454d3052b Generate admin password on db:seed
Test admin account creation in db:seed
2026-04-30 18:23:00 +02:00
cryptogopher dedea0246f Run tests on multiple database configurations 2026-04-27 17:38:49 +02:00
cryptogopher 3ac3a6f13c Drop Readout.value decimal type in favor of float 2026-04-25 18:20:16 +02:00
cryptogopher 97e7bfedf7 Upgrade Rails to 8.0.5
Install sqlite3
Migrate Rails default settings from 7.2 to 8.0
2026-04-25 04:13:47 +02:00
cryptogopher a05767213c Fix test based on unavailable td.link selector 2026-04-25 04:13:47 +02:00
cryptogopher d002ae0f2c Clean up and improve items-table styling
Closes #9
2026-04-25 04:13:47 +02:00
cryptogopher f87aa9fd3d Improve Measurement form layout and styling 2026-04-25 04:13:47 +02:00
137 changed files with 2053 additions and 1242 deletions
+5 -1
View File
@@ -15,6 +15,9 @@
/config/master.key /config/master.key
/config/puma.rb /config/puma.rb
# Ignore test database.
/db/test.sqlite3
# Ignore all logfiles and tempfiles. # Ignore all logfiles and tempfiles.
/log/* /log/*
/tmp/* /tmp/*
@@ -30,7 +33,7 @@
/tmp/restart.txt /tmp/restart.txt
# Ignore user files # Ignore user files.
/.bash_history /.bash_history
/.byebug_history /.byebug_history
/.config /.config
@@ -38,6 +41,7 @@
/.lesshst /.lesshst
/.local /.local
/.mysql_history /.mysql_history
/.sqlite_history
/.ssh /.ssh
/.vim /.vim
/.viminfo /.viminfo
+76
View File
@@ -0,0 +1,76 @@
DESIGN
======
Below is a list of design decisions. The justification is to be consulted
whenever a change is considered, to avoid regressions.
### Data type for DB storage of numeric values (`decimal` vs `float`)
* among database engines supported (by Rails), SQLite offers storage of
`decimal` data type with the lowest precision, equal to the precision of
`REAL` type (double precision float value, IEEE 754), but in a floating point
format,
* decimal types in other database engines offer greater precision, but store
data in a fixed point format,
* biology-related values differ by several orders of magnitude; storing them in
fixed point format would only make sense if required precision would be
greater than that offered by floating point format,
* even then, fixed point would mean either bigger memory requirements or
worse precision for numbers close to scale limit,
* for a fixed point format to use the same 8 bytes of storage as IEEE
754, precision would need to be limited to 18 digits (4 bytes/9 digits)
and scale approximately half of that - 9,
* double precision floating point guarantees 15 digits of precision, which
is more than enough for all expected use cases,
* if a decimal string with at most 15 significant digits is converted to
the IEEE 754 double-precision format, giving a normal number, and then
converted back to a decimal string with the same number of digits, the
final result should match the original string,
* if an IEEE 754 double-precision number is converted to a decimal
string with at least 17 significant digits, and then converted back to
double-precision representation, the final result must match the
original number,
* single precision floating point only guarntees 6 digits of precision,
which is estimated to be too low for some use cases (e.g. storing
latitude/longitude with a resolution grater than 100m),
* double precision floating point (IEEE 754) is a standard that ensures
compatibility with majority of database engines,
* the same data format is used internally by Ruby as a `Float`; it
guarantees no conversions between storage and computation,
* as a standard with hardware implementations ensures both: computing
efficiency and hardware/3rd party library compatibility as opposed to Ruby
custom `BigDecimal` type,
* at present, only normalized numbers are used and considered sufficient, in
order to avoid potential issues with the cross-platform compatibility of
subnormal (denormal) numbers,
* in the future, the IEEE 754 decimal64 data type may be considered once it is
supported by database engines.
### Database layer vs application layer data model constraints
* database constraints are the final guard against data integrity corruption,
* they should safeguard against data referential integrity loss under _all_
data (not schema) manipulation scenarios, including application level
logic errors and direct data manipulation (e.g. through `dbconsole`),
* application constraints can be as restrictive as database constraints or more,
but not less, as it doesn't serve any use case,
* proper application level constraints should prevent unhandled database
exception occurences, e.g `ActiveRecord::InvalidForeignKey` for operations
performed through Models (i.e. not `#delete_all` etc.)
### User interface
* values provided by user have to be exactly preserved, avoiding any precision
loss due to e.g. type casting
* when it's not possible to store value without information loss, feedback has to
be given at the time of entering value
* values can be received using either:
* `input[number]` - validates: number format, value against `[min]` and
`[max]` attributes; cannot validate precision (no `[pattern]` support);
or
* `input[text]` - can validate number format and precision with
`[pattern]`; cannot validate range (no `[min]`/`[max]` support),
* none of the above prevents misinterpretation of values between 0 and
Float::MIN as 0.0 - this has to be checked manually,
* assuming that user input with _precision > DIG_ is as rare as in
the range _(0, MIN)_, the decision is to choose input which requires
the least/simplest additional code.
+8 -11
View File
@@ -1,14 +1,14 @@
source "https://rubygems.org" source "https://rubygems.org"
# The requirement for the Ruby version comes from Rails # The requirement for the Ruby version comes from Rails.
gem "rails", "~> 7.2.3" gem "rails", "~> 8.1.4"
gem "sprockets-rails" gem "sprockets-rails"
gem "puma", "~> 6.0" gem "puma", "~> 6.0"
gem "sassc-rails" gem "sassc-rails"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
# TODO: select db gems automatically # TODO: select db gems automatically?
# database_config = ERB.new(File.read("config/database.yml")).result # database_config = ERB.new(File.read("config/database.yml")).result
# YAML.load(database_config, aliases: true).values.map { |env| env["adapter"] }.uniq # YAML.load(database_config, aliases: true).values.map { |env| env["adapter"] }.uniq
group :mysql, optional: true do group :mysql, optional: true do
@@ -21,7 +21,7 @@ group :sqlite, optional: true do
gem "sqlite3", "~> 2.7" gem "sqlite3", "~> 2.7"
end end
gem "devise" gem "devise", "~> 5.0.4"
gem "importmap-rails" gem "importmap-rails"
gem "turbo-rails", "~> 2.0" gem "turbo-rails", "~> 2.0"
@@ -42,11 +42,8 @@ group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara" gem "capybara"
gem "selenium-webdriver" gem "selenium-webdriver"
# Remove minitest version restriction after error fixed:
# railties-7.2.3/lib/rails/test_unit/line_filtering.rb:7:in `run':
# wrong number of arguments (given 3, expected 1..2) (ArgumentError)
# from /var/www/.gem/ruby/3.3.0/gems/minitest-6.0.2/lib/minitest.rb:473:in
# `block (2 levels) in run_suite'
gem "minitest", "< 6"
end end
# TODO: remove after upgrade to next version containing PR:
# https://github.com/ruby-i18n/i18n/pull/755
gem "i18n", git: "https://github.com/ruby-i18n/i18n", branch: "delegate-missing-interpolation-handler"
+151 -145
View File
@@ -1,85 +1,93 @@
GIT
remote: https://github.com/ruby-i18n/i18n
revision: 8850d979a3a32453123ed744b943ead8751ba73b
branch: delegate-missing-interpolation-handler
specs:
i18n (1.15.2)
concurrent-ruby (~> 1.0)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.2.3) action_text-trix (2.1.19)
actionpack (= 7.2.3) railties
activesupport (= 7.2.3) actioncable (8.1.4)
actionpack (= 8.1.4)
activesupport (= 8.1.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.2.3) actionmailbox (8.1.4)
actionpack (= 7.2.3) actionpack (= 8.1.4)
activejob (= 7.2.3) activejob (= 8.1.4)
activerecord (= 7.2.3) activerecord (= 8.1.4)
activestorage (= 7.2.3) activestorage (= 8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (7.2.3) actionmailer (8.1.4)
actionpack (= 7.2.3) actionpack (= 8.1.4)
actionview (= 7.2.3) actionview (= 8.1.4)
activejob (= 7.2.3) activejob (= 8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.2.3) actionpack (8.1.4)
actionview (= 7.2.3) actionview (= 8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
cgi
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc rack (>= 2.2.4)
rack (>= 2.2.4, < 3.3)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (7.2.3) actiontext (8.1.4)
actionpack (= 7.2.3) action_text-trix (~> 2.1.15)
activerecord (= 7.2.3) actionpack (= 8.1.4)
activestorage (= 7.2.3) activerecord (= 8.1.4)
activesupport (= 7.2.3) activestorage (= 8.1.4)
activesupport (= 8.1.4)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.2.3) actionview (8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
builder (~> 3.1) builder (~> 3.1)
cgi
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.2.3) activejob (8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.2.3) activemodel (8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
activerecord (7.2.3) activerecord (8.1.4)
activemodel (= 7.2.3) activemodel (= 8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.2.3) activestorage (8.1.4)
actionpack (= 7.2.3) actionpack (= 8.1.4)
activejob (= 7.2.3) activejob (= 8.1.4)
activerecord (= 7.2.3) activerecord (= 8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.2.3) activesupport (8.1.4)
base64 base64
benchmark (>= 0.3)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
drb drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
json
logger (>= 1.4.2) logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.8) uri (>= 0.13.1)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0) public_suffix (>= 2.0.2, < 8.0)
base64 (0.3.0) base64 (0.3.0)
bcrypt (3.1.21) bcrypt (3.1.22)
benchmark (0.5.0) bigdecimal (4.1.3)
bigdecimal (4.0.1)
bindex (0.8.1) bindex (0.8.1)
builder (3.3.0) builder (3.3.0)
byebug (13.0.0) byebug (13.0.0)
@@ -93,155 +101,155 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
cgi (0.5.1) concurrent-ruby (1.3.8)
concurrent-ruby (1.3.6)
connection_pool (3.0.2) connection_pool (3.0.2)
crass (1.0.6) crass (1.0.7)
date (3.5.1) date (3.5.1)
devise (5.0.2) devise (5.0.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 7.0) railties (>= 7.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
drb (2.2.3) drb (2.2.3)
erb (6.0.2) erb (6.0.7)
erubi (1.13.1) erubi (1.13.1)
ffi (1.17.3-aarch64-linux-gnu) ffi (1.17.4-aarch64-linux-gnu)
ffi (1.17.3-aarch64-linux-musl) ffi (1.17.4-aarch64-linux-musl)
ffi (1.17.3-arm-linux-gnu) ffi (1.17.4-arm-linux-gnu)
ffi (1.17.3-arm-linux-musl) ffi (1.17.4-arm-linux-musl)
ffi (1.17.3-arm64-darwin) ffi (1.17.4-arm64-darwin)
ffi (1.17.3-x86_64-darwin) ffi (1.17.4-x86_64-darwin)
ffi (1.17.3-x86_64-linux-gnu) ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.3-x86_64-linux-musl) ffi (1.17.4-x86_64-linux-musl)
globalid (1.3.0) globalid (1.4.0)
activesupport (>= 6.1) activesupport (>= 6.1)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
importmap-rails (2.2.3) importmap-rails (2.2.3)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activesupport (>= 6.0.0) activesupport (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
io-console (0.8.2) io-console (0.9.4)
irb (1.17.0) irb (1.18.0)
pp (>= 0.6.0) pp (>= 0.6.0)
prism (>= 1.3.0) prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
json (3.0.2)
logger (1.7.0) logger (1.7.0)
loofah (2.25.0) loofah (2.25.2)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.9.0) mail (2.9.1)
logger logger
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
marcel (1.1.0) marcel (1.2.1)
matrix (0.4.3) matrix (0.4.3)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (5.27.0) minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
mysql2 (0.5.7) mysql2 (0.5.7)
bigdecimal bigdecimal
net-imap (0.6.3) net-imap (0.6.7)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.4.0)
timeout timeout
net-smtp (0.5.1) net-smtp (0.5.2)
net-protocol net-protocol
nio4r (2.7.5) nio4r (2.7.5)
nokogiri (1.19.1-aarch64-linux-gnu) nokogiri (1.19.4-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-aarch64-linux-musl) nokogiri (1.19.4-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-arm-linux-gnu) nokogiri (1.19.4-arm-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-arm-linux-musl) nokogiri (1.19.4-arm-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-arm64-darwin) nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-x86_64-darwin) nokogiri (1.19.4-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu) nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl) nokogiri (1.19.4-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pg (1.6.3) pg (1.7.0)
pg (1.6.3-aarch64-linux) pg (1.7.0-aarch64-linux)
pg (1.6.3-aarch64-linux-musl) pg (1.7.0-aarch64-linux-musl)
pg (1.6.3-arm64-darwin) pg (1.7.0-arm64-darwin)
pg (1.6.3-x86_64-darwin) pg (1.7.0-x86_64-darwin)
pg (1.6.3-x86_64-linux) pg (1.7.0-x86_64-linux)
pg (1.6.3-x86_64-linux-musl) pg (1.7.0-x86_64-linux-musl)
pp (0.6.3) pp (0.6.4)
prettyprint prettyprint
prettyprint (0.2.0) prettyprint (0.2.0)
prism (1.9.0) prism (1.9.0)
psych (5.3.1) public_suffix (7.0.5)
date
stringio
public_suffix (7.0.2)
puma (6.6.1) puma (6.6.1)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.5) rack (3.2.7)
rack-session (2.1.1) rack-session (2.1.2)
base64 (>= 0.1.0) base64 (>= 0.1.0)
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.2.0) rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.3.1) rackup (2.3.1)
rack (>= 3) rack (>= 3)
rails (7.2.3) rails (8.1.4)
actioncable (= 7.2.3) actioncable (= 8.1.4)
actionmailbox (= 7.2.3) actionmailbox (= 8.1.4)
actionmailer (= 7.2.3) actionmailer (= 8.1.4)
actionpack (= 7.2.3) actionpack (= 8.1.4)
actiontext (= 7.2.3) actiontext (= 8.1.4)
actionview (= 7.2.3) actionview (= 8.1.4)
activejob (= 7.2.3) activejob (= 8.1.4)
activemodel (= 7.2.3) activemodel (= 8.1.4)
activerecord (= 7.2.3) activerecord (= 8.1.4)
activestorage (= 7.2.3) activestorage (= 8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.2.3) railties (= 8.1.4)
rails-dom-testing (2.3.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.7.0) rails-html-sanitizer (1.7.1)
loofah (~> 2.25) loofah (~> 2.25, >= 2.25.2)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (7.2.3) railties (8.1.4)
actionpack (= 7.2.3) actionpack (= 8.1.4)
activesupport (= 7.2.3) activesupport (= 8.1.4)
cgi
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2) tsort (>= 0.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rake (13.3.1) rake (13.4.2)
rdoc (7.2.0) rbs (4.2.0)
erb logger
psych (>= 4.0.0) prism (>= 1.6.0)
tsort tsort
regexp_parser (2.11.3) rdoc (8.1.0)
reline (0.6.3) erb
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
regexp_parser (2.13.1)
reline (0.7.0)
io-console (~> 0.5) io-console (~> 0.5)
responders (3.2.0) responders (3.2.1)
actionpack (>= 7.0) actionpack (>= 7.0)
railties (>= 7.0) railties (>= 7.0)
rexml (3.4.4) rubyzip (3.7.0)
rubyzip (3.2.2)
sassc (2.4.0) sassc (2.4.0)
ffi (~> 1.9) ffi (~> 1.9)
sassc-rails (2.1.2) sassc-rails (2.1.2)
@@ -251,54 +259,52 @@ GEM
sprockets-rails sprockets-rails
tilt tilt
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.41.0) selenium-webdriver (4.50.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0) rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0) websocket (~> 1.0)
sprockets (4.2.2) sprockets (4.4.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.1)
logger logger
rack (>= 2.2.4, < 4) rack (>= 2.2.4, < 4)
sprockets-rails (3.5.2) sprockets-rails (3.5.2)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (2.9.0-aarch64-linux-gnu) sqlite3 (2.9.6-aarch64-linux-gnu)
sqlite3 (2.9.0-aarch64-linux-musl) sqlite3 (2.9.6-aarch64-linux-musl)
sqlite3 (2.9.0-arm-linux-gnu) sqlite3 (2.9.6-arm-linux-gnu)
sqlite3 (2.9.0-arm-linux-musl) sqlite3 (2.9.6-arm-linux-musl)
sqlite3 (2.9.0-arm64-darwin) sqlite3 (2.9.6-arm64-darwin)
sqlite3 (2.9.0-x86_64-darwin) sqlite3 (2.9.6-x86_64-darwin)
sqlite3 (2.9.0-x86_64-linux-gnu) sqlite3 (2.9.6-x86_64-linux-gnu)
sqlite3 (2.9.0-x86_64-linux-musl) sqlite3 (2.9.6-x86_64-linux-musl)
stringio (3.2.0)
thor (1.5.0) thor (1.5.0)
tilt (2.7.0) tilt (2.9.0)
timeout (0.6.0) timeout (0.6.1)
tsort (0.2.0) tsort (0.2.0)
turbo-rails (2.0.23) turbo-rails (2.0.23)
actionpack (>= 7.1.0) actionpack (>= 7.1.0)
railties (>= 7.1.0) railties (>= 7.1.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
uri (1.1.1)
useragent (0.16.11) useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.3.0)
actionview (>= 6.0.0) actionview (>= 8.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 8.0.0)
websocket (1.2.11) websocket (1.2.11)
websocket-driver (0.8.0) websocket-driver (0.8.2)
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.5) zeitwerk (2.8.3)
PLATFORMS PLATFORMS
aarch64-linux-gnu aarch64-linux-gnu
@@ -313,13 +319,13 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
byebug byebug
capybara capybara
devise devise (~> 5.0.4)
i18n!
importmap-rails importmap-rails
minitest (< 6)
mysql2 (~> 0.5) mysql2 (~> 0.5)
pg (~> 1.5) pg (~> 1.5)
puma (~> 6.0) puma (~> 6.0)
rails (~> 7.2.3) rails (~> 8.1.4)
sassc-rails sassc-rails
selenium-webdriver selenium-webdriver
sprockets-rails sprockets-rails
+43 -17
View File
@@ -16,14 +16,10 @@ environment, see the _Contributing_ section below.
* Server side: * Server side:
* Ruby interpreter, depending on the version of Rails used (see _Gemfile_), * Ruby interpreter, depending on the version of Rails used (see _Gemfile_),
* https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#ruby-versions * https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#ruby-versions
* database (e.g. MySQL >= 8.0) supporting: * database (e.g. SQLite, MySQL >= 8.0) supporting:
* recursive Common Table Expressions (CTE) for SELECT/UPDATE/DELETE, * recursive Common Table Expressions (CTE) for SELECT/UPDATE/DELETE,
* MariaDB does not support CTE for UPDATE/DELETE * MariaDB does not support CTE for UPDATE/DELETE
(https://jira.mariadb.org/browse/MDEV-18511) (https://jira.mariadb.org/browse/MDEV-18511)
* decimal datatype with precision of at least 30,
* SQLite3 _flexible typing_ decimal will work, but precision
will be limited to 16, making it practical mostly for testing
purposes
* for testing: browser as specified in _Client side_ requirements * for testing: browser as specified in _Client side_ requirements
* Client side: * Client side:
* browser (e.g. Firefox >= 121) supporting: * browser (e.g. Firefox >= 121) supporting:
@@ -89,14 +85,10 @@ Running
### Standalone Rails server + Apache proxy ### Standalone Rails server + Apache proxy
Customize Puma config template: Copy and customize Puma config template if required:
cp -a config/puma.rb.dist config/puma.rb cp -a config/puma.rb.dist config/puma.rb
and specify server IP/port, either with `port` or `bind`, e.g.:
bind 'tcp://0.0.0.0:3000'
#### (option 1) Start server manually #### (option 1) Start server manually
bundle exec rails s -e production bundle exec rails s -e production
@@ -123,9 +115,26 @@ Contributing
### Gems ### Gems
Apart from database adapter, install development and testing gems: Install development and testing gems, including at least MySQL and SQLite
database adapters:
bundle config --local with mysql development test bundle config --local with development test mysql sqlite
#### Using gems from local git repos
When working on gem PRs, it may be useful to work with proposed changes locally
until PRs get accepted upstream. To add gem hosted in local git repository:
bundle config set local.capybara /var/www/tmp/capybara
Add gem to `Gemfile`:
gem "capybara", git: "https://github.com/teamcapybara/capybara", branch: "branch-name"
and install:
bundle install
bundle show capybara
### Configuration ### Configuration
@@ -138,20 +147,29 @@ assets.
### Database ### Database
Grant database user privileges for development and test environments, Grant database user privileges for development and test environments. Example
possibly with different Ruby versions: below shows how to grant privileges to all databases which names start with
`fixinme-` on MySQL:
> mysql -p > mysql -p
mysql> create user `fixinme-dev`@localhost identified by '<some password>'; mysql> create user `fixinme-dev`@localhost identified by '<some password>';
mysql> grant all privileges on `fixinme-%`.* to `fixinme-dev`@localhost; mysql> grant all privileges on `fixinme-%`.* to `fixinme-dev`@localhost;
mysql> flush privileges; mysql> flush privileges;
Dumping development data before database reset:
mysqldump -h <address> -u <user> -p --no-create-info --no-tablespaces --complete-insert <database> > tmp/data.sql
### Development environment ### Development environment
Starting application server in development environment: Starting application server in development environment:
bundle exec rails s -e development bundle exec rails s -e development
Accessing database console when more than one test db is present:
bundle exec rails dbconsole -e test --db sqlite3
For running rake tasks, prepend command with environment: For running rake tasks, prepend command with environment:
RAILS_ENV=development bundle exec rails ... RAILS_ENV=development bundle exec rails ...
@@ -164,14 +182,22 @@ Tests need to be run from within toplevel application directory:
bundle exec rails test:system bundle exec rails test:system
* system test(s) with seed/test name specified: * system test(s) with seed or test name specified:
bundle exec rails test:system --seed 64690 --name test_add_unit bundle exec rails test:system --include test_add_unit --seed 64690
* all tests from one file, with optional seed: * all tests from one file, optionally with seed:
bundle exec rails test test/system/users_test.rb --seed 1234 bundle exec rails test test/system/users_test.rb --seed 1234
* system tests for selected database configuration (if multiple present):
bundle exec rails test:system --include /^test_sqlite3_/
* single system test for all database configurations (if multiple present):
bundle exec rails test:system --include /^test_\\w+_add_unit$/
### Icons ### Icons
Pictogrammers Material Design Icons: https://pictogrammers.com/library/mdi/ Pictogrammers Material Design Icons: https://pictogrammers.com/library/mdi/

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 432 B

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 432 B

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 524 B

Before

Width:  |  Height:  |  Size: 618 B

After

Width:  |  Height:  |  Size: 618 B

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 152 B

Before

Width:  |  Height:  |  Size: 151 B

After

Width:  |  Height:  |  Size: 151 B

Before

Width:  |  Height:  |  Size: 236 B

After

Width:  |  Height:  |  Size: 236 B

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

Before

Width:  |  Height:  |  Size: 231 B

After

Width:  |  Height:  |  Size: 231 B

Before

Width:  |  Height:  |  Size: 148 B

After

Width:  |  Height:  |  Size: 148 B

Before

Width:  |  Height:  |  Size: 382 B

After

Width:  |  Height:  |  Size: 382 B

Before

Width:  |  Height:  |  Size: 330 B

After

Width:  |  Height:  |  Size: 330 B

Before

Width:  |  Height:  |  Size: 350 B

After

Width:  |  Height:  |  Size: 350 B

Before

Width:  |  Height:  |  Size: 189 B

After

Width:  |  Height:  |  Size: 189 B

Before

Width:  |  Height:  |  Size: 192 B

After

Width:  |  Height:  |  Size: 192 B

Before

Width:  |  Height:  |  Size: 174 B

After

Width:  |  Height:  |  Size: 174 B

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 597 B

Before

Width:  |  Height:  |  Size: 216 B

After

Width:  |  Height:  |  Size: 216 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 771 B

After

Width:  |  Height:  |  Size: 771 B

Before

Width:  |  Height:  |  Size: 671 B

After

Width:  |  Height:  |  Size: 671 B

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 220 B

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 218 B

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 316 B

Before

Width:  |  Height:  |  Size: 297 B

After

Width:  |  Height:  |  Size: 297 B

Before

Width:  |  Height:  |  Size: 197 B

After

Width:  |  Height:  |  Size: 197 B

Before

Width:  |  Height:  |  Size: 165 B

After

Width:  |  Height:  |  Size: 165 B

Before

Width:  |  Height:  |  Size: 242 B

After

Width:  |  Height:  |  Size: 242 B

Before

Width:  |  Height:  |  Size: 423 B

After

Width:  |  Height:  |  Size: 423 B

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 173 B

Before

Width:  |  Height:  |  Size: 475 B

After

Width:  |  Height:  |  Size: 475 B

+189 -182
View File
@@ -18,14 +18,13 @@
/* Strive for simplicity: /* Strive for simplicity:
* * style elements/tags only - if possible, * * style elements/tags only - if possible,
* * replace element/tag name with class name - if element has to be styled * * replace element/tag name with class name - if element has to be styled
* differently depending on context (e.g. <form>; <a> as link/button), * differently depending on context (e.g. <form>, <table>, <a> as link/button),
* * styles with multiple selectors should have all selectors with same * * styles with multiple selectors should have all selectors with same
* specificity, to allow proper rule specificity vs order management. * specificity, to allow proper rule specificity vs order management.
* *
* NOTE: style in a modular way, similar to how CSS @scope would be used, * NOTE: style in a modular way, similar to how CSS @scope would be used,
* to make transition easier once @scope is widely available. */ * to make transition easier once @scope is widely available. */
/* TODO: review styles with multiple selectors and try to convert them to the same /* TODO: change :root -> <html> ? */
* specificity. */
:root { :root {
--color-focus-gray: #f3f3f3; --color-focus-gray: #f3f3f3;
--color-border-gray: #dddddd; --color-border-gray: #dddddd;
@@ -57,17 +56,24 @@
:focus-visible { :focus-visible {
outline: none; outline: none;
} }
/* NOTE: move to higher priority layer instead of using !important? */ /* NOTE: move to higher priority layer instead of using !important?; add CSS
* @layer requirements in README */
/* Disable elements with:
* `[aria-disabled=true]` - non-form elements (e.g. links),
* `[disabled]` - form controls, that may be enabled conditionally,
* `[readonly]` - form controls, that stay disabled for form lifetime. */
[aria-disabled=true],
[disabled] { [disabled] {
border-color: var(--color-border-gray) !important; border-color: var(--color-border-gray) !important;
color: var(--color-border-gray) !important; color: var(--color-border-gray) !important;
/* NOTE: cannot set cursor with `pointer-events: none`; can be fixed by setting /* NOTE: cannot set cursor when `pointer-events: none`; can be fixed by setting
* `cursor` on wrapping element. * `cursor` on wrapping element.
cursor: not-allowed; */ cursor: not-allowed; */
fill: var(--color-border-gray) !important; fill: var(--color-border-gray) !important;
pointer-events: none !important; pointer-events: none !important;
} }
/* [hidden] submit elements cannot have `display` set as it makes them visible. */ /* Styles set `display` without distinguishing between [hidden] elements, making
* them visible. */
[hidden] { [hidden] {
display: none !important; display: none !important;
} }
@@ -78,10 +84,13 @@
* gray - target for interaction with keyboard, * gray - target for interaction with keyboard,
* red - destructive, non-undoable action. * red - destructive, non-undoable action.
*/ */
/* TODO: merge selectors using :is() */
a,
button, button,
details, details,
input, input,
select, select,
summary,
textarea { textarea {
background-color: inherit; background-color: inherit;
font: inherit; font: inherit;
@@ -99,7 +108,14 @@ textarea {
border-radius: 0.25em; border-radius: 0.25em;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
} }
svg, svg {
height: 1.4em;
margin: 0 0.2em 0 0;
width: 1.4em;
}
svg:last-child {
margin-right: 0;
}
textarea { textarea {
margin: 0; margin: 0;
} }
@@ -122,6 +138,7 @@ input[type=checkbox]:checked {
input[type=number] { input[type=number] {
appearance: textfield; appearance: textfield;
-moz-appearance: textfield; -moz-appearance: textfield;
text-align: end;
} }
input::-webkit-inner-spin-button { input::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
@@ -130,7 +147,32 @@ input::-webkit-outer-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
margin: 0; margin: 0;
} }
/* Text color of table form controls:
* - black for row/table forms,
* - inherited for internal (column specific) buttons/forms. */
table input,
table select,
table summary,
table textarea {
border-color: var(--color-border-gray);
}
table input,
table select,
table textarea {
padding-block: 0.375em;
}
table form input,
table form select,
table form summary,
table form textarea {
color: inherit;
}
table svg:not(:only-child) {
height: 1.25em;
width: 1.25em;
}
input:focus-visible, input:focus-visible,
/* TODO: select:focus-visible is redundant with -within? */
select:focus-visible, select:focus-visible,
select:focus-within, select:focus-within,
/* TODO: how to achieve `summary:focus-within` for `::details-content`? */ /* TODO: how to achieve `summary:focus-within` for `::details-content`? */
@@ -138,6 +180,7 @@ summary:focus-visible,
textarea:focus-visible { textarea:focus-visible {
accent-color: var(--color-dark-blue); accent-color: var(--color-dark-blue);
background-color: var(--color-focus-gray); background-color: var(--color-focus-gray);
color: black;
} }
input:hover, input:hover,
select:hover, select:hover,
@@ -148,14 +191,25 @@ textarea:hover {
} }
select:hover, select:hover,
summary:hover { summary:hover {
color: black;
cursor: pointer; cursor: pointer;
} }
/* TODO: style <details>/<summary> focus to match <select> as much as possible.
summary:focus-visible::before,
summary:hover::before {
background-color: black;
}
*/
input:invalid, input:invalid,
select:invalid, select:invalid,
textarea:invalid { textarea:invalid {
border-color: var(--color-red); border-color: var(--color-red);
outline-color: var(--color-red); outline-color: var(--color-red);
} }
pre {
display: inline;
font: inherit;
}
/* `.button`: button-styled <a>, <button>, <input type=submit>. /* `.button`: button-styled <a>, <button>, <input type=submit>.
* `.link`: any other <a>. * `.link`: any other <a>.
@@ -183,20 +237,21 @@ textarea:invalid {
padding: 0.6em 0.5em; padding: 0.6em 0.5em;
width: fit-content; width: fit-content;
} }
[name=cancel], .link {
color: inherit;
text-decoration: underline 1px var(--color-border-gray);
text-underline-offset: 0.25em;
}
.auxiliary { .auxiliary {
border-color: var(--color-border-gray); border-color: var(--color-nav-gray);
color: var(--color-nav-gray); color: var(--color-nav-gray);
fill: var(--color-nav-gray); fill: var(--color-nav-gray);
} }
.button > svg, table .button {
.tab > svg { border-color: var(--color-border-gray);
height: 1.4em; font-weight: normal;
width: 1.4em; height: 100%;
} padding: 0.4em;
.button > svg:not(:last-child),
.tab > svg:not(:last-child) {
margin-right: 0.2em;
} }
.button:focus-visible, .button:focus-visible,
.tab:focus-visible, .tab:focus-visible,
@@ -213,13 +268,12 @@ textarea:invalid {
background-color: var(--color-red); background-color: var(--color-red);
border-color: var(--color-red); border-color: var(--color-red);
} }
/* TODO: move normal, non-button links (<a>:hover/:focus) styling here (i.e. .link:focus-visible {
* page-wide, top-level) and remove from `table.items` - as the style should be text-decoration-color: var(--color-gray);
* same everywhere. */ }
input[type=text]:read-only, .link:hover {
textarea:read-only { color: var(--color-blue);
border: none; text-decoration-color: var(--color-blue);
padding-inline: 0;
} }
@@ -232,16 +286,16 @@ body {
grid-template-areas: grid-template-areas:
"header header header" "header header header"
"nav nav nav" "nav nav nav"
"leftside topside rightside"
"leftside main rightside"; "leftside main rightside";
grid-template-columns: 1fr minmax(max-content, 2fr) 1fr; grid-template-columns: 1fr minmax(max-content, 2fr) 1fr;
font-family: system-ui; font-family: system-ui;
margin: 0.4em; margin: 0.4em;
} }
body:not(:has(.topside-area)) { body:has(> .topside-area) {
grid-template-areas: grid-template-areas:
"header header header" "header header header"
"nav nav nav" "nav nav nav"
"leftside topside rightside"
"leftside main rightside"; "leftside main rightside";
} }
@@ -249,6 +303,7 @@ header {
grid-area: header; grid-area: header;
} }
/* TODO: change .navigation -> <nav> ? */
.navigation { .navigation {
display: flex; display: flex;
grid-area: nav; grid-area: nav;
@@ -315,21 +370,21 @@ header {
line-height: 2.2em; line-height: 2.2em;
pointer-events: auto; pointer-events: auto;
} }
.flash:before { .flash::before {
filter: invert(); filter: invert(1);
height: 1.4em; height: 1.4em;
margin: 0 0.5em; margin: 0 0.5em;
width: 1.4em; width: 1.4em;
} }
.flash.alert:before { .flash.alert::before {
content: url('pictograms/alert-outline.svg'); content: url('alert-outline.svg');
} }
.flash.alert { .flash.alert {
border-color: var(--color-red); border-color: var(--color-red);
background-color: var(--color-red); background-color: var(--color-red);
} }
.flash.notice:before { .flash.notice::before {
content: url('pictograms/check-circle-outline.svg'); content: url('check-circle-outline.svg');
} }
.flash.notice { .flash.notice {
border-color: var(--color-blue); border-color: var(--color-blue);
@@ -390,79 +445,83 @@ header {
padding-block: 0; padding-block: 0;
} }
.tabular-form table {
/* TODO: remove `.items` class (?) and make `form table` work properly. */ border: none;
table.items {
border-spacing: 0; border-spacing: 0;
border: 1px solid var(--color-border-gray); }
.tabular-form table td {
border: none;
padding-inline-start: 0.4em;
vertical-align: middle;
}
.tabular-form table td:first-child {
padding-inline-start: 0;
}
.tabular-form table td:last-child {
padding-inline-end: 0;
}
.tabular-form table :is(form, input, select, textarea):only-child {
margin-inline-start: 0;
}
.items-table {
border: none;
border-radius: 0.25em; border-radius: 0.25em;
border-spacing: 0;
font-size: 0.85rem; font-size: 0.85rem;
text-align: left; text-align: left;
} }
table:not(:has(tr)) { .items-table:has(tbody > tr > td) {
border: 1px solid var(--color-border-gray);
}
.items-table:has(tbody > tr > td) caption {
display: none; display: none;
} }
table.items thead { .items-table tbody:not(:has(tr > td)){
font-size: 0.8rem; visibility: collapse;
} }
table.items thead, .items-table th,
table.items tbody tr:hover { .items-table tr:where(:hover) {
background-color: var(--color-focus-gray); background-color: var(--color-focus-gray);
} }
table.items th { .items-table th {
padding-block: 0.75em; font-size: 0.8rem;
padding: 0.75em 0 0.75em 1em;
text-align: center; text-align: center;
} }
table.items th, .items-table th:last-child {
table.items td {
padding-inline: 1em 0;
}
/* For <a> to fill <td> completely, we use an `::after` pseudoelement. */
table.items td.link {
padding: 0;
position: relative;
}
table.items td.link a {
color: inherit;
font: inherit;
}
table.items td.link a::after {
content: '';
inset: 0;
position: absolute;
}
table.items td:first-child {
padding-inline-start: calc(1em + var(--depth) * 0.8em);
}
table.items td:has(input, select, textarea) {
padding-inline-start: calc(0.6em - 0.9px);
}
table.items td:first-child:has(input, select, textarea) {
padding-inline-start: calc(0.6em + var(--depth) * 0.8em - 0.9px);
}
table.items th:last-child {
padding-inline-end: 0.4em; padding-inline-end: 0.4em;
} }
table.items td:last-child { .items-table td {
padding-inline-end: 0.1em;
}
table.items td {
border-top: 1px solid var(--color-border-gray); border-top: 1px solid var(--color-border-gray);
height: 2.4em; height: 2.4em;
padding-block: 0.1em; padding: 0.1em 0 0.1em calc(1em + var(--depth) * 0.8em);
} }
table.items .actions { .items-table td:last-child {
display: flex; padding-inline-end: 0.1em;
}
.items-table :is(form, input, select, textarea):only-child {
margin-inline-start: calc(-0.4em - 0.9px);
}
/* For <a> to fill table cell completely, we use an `::after` pseudoelement. */
/* TODO: expand to whole row? will require adjusting z-index on inputs/buttons */
.items-table td:has(> .link) {
position: relative;
}
.items-table .link::after {
content: '';
inset: -1px 0 0 0;
position: absolute;
}
.items-table .flex {
gap: 0.4em; gap: 0.4em;
justify-content: end; justify-content: end;
} }
table.items .actions.centered { .items-table .dropzone {
justify-content: center;
}
table.items tr.dropzone {
position: relative; position: relative;
} }
table.items tr.dropzone::after { .items-table .dropzone::after {
content: ''; content: '';
inset: 1px 0 0 0; inset: 1px 0 0 0;
position: absolute; position: absolute;
@@ -470,116 +529,37 @@ table.items tr.dropzone::after {
outline-offset: -1px; outline-offset: -1px;
z-index: var(--z-index-table-row-outline); z-index: var(--z-index-table-row-outline);
} }
table.items td.handle { .items-table .handle {
cursor: move; cursor: grab;
} }
table.items tr.form td { .items-table .form td {
vertical-align: top; vertical-align: top;
} }
.items-table td:not(:first-child),
/* TODO: replace `:hover:focus-visible` combos with proper LOVE style order. */
/* TODO: update table styling: simplify selectors, deduplicate, remove non-font rem. */
table.items td.link a:hover,
table.items td.link a:focus-visible,
table.items td.link a:hover:focus-visible {
text-decoration: underline;
text-decoration-thickness: 0.05rem;
text-underline-offset: 0.2rem;
}
table.items td.link a:hover {
color: var(--color-blue);
}
table.items td.link a:focus-visible {
text-decoration-color: var(--color-gray);
}
table.items td.link a:hover:focus-visible {
color: var(--color-dark-blue);
}
table.items td:not(:first-child),
.grayed { .grayed {
color: var(--color-table-gray); color: var(--color-table-gray);
fill: var(--color-table-gray); fill: var(--color-gray);
} }
table.items svg { .items-table td:has(> svg:only-child) {
height: 1rem;
vertical-align: middle;
width: 1rem;
}
table.items svg:last-child {
height: 1.2rem;
width: 1.2rem;
}
table.items td.svg {
text-align: center; text-align: center;
} }
table.items td.number {
text-align: right;
}
table.items .button {
font-weight: normal;
height: 100%;
padding: 0.4em;
}
table.items input:not([type=submit]):not([type=checkbox]),
table.items select,
table.items textarea {
padding-block: 0.375em;
}
table input,
table select,
table summary,
table textarea {
border-color: var(--color-border-gray);
}
table select {
color: var(--color-table-gray);
}
table select:hover,
table select:focus-within,
table select:focus-visible {
color: black;
}
table .button {
border-color: var(--color-border-gray);
color: var(--color-table-gray);
}
form table.items {
border: none;
}
form table.items td {
border: none;
text-align: left;
vertical-align: middle;
}
form table.items td:first-child {
color: inherit;
}
form table select {
color: black;
}
.centered { .center {
margin: 0 auto; margin: 0 auto;
} }
.extendedright {
margin-right: auto;
}
.hexpand { .hexpand {
width: 100%; width: 100%;
} }
.hflex { .flex {
display: flex; display: flex;
gap: 0.8em; gap: 0.8em;
} }
.hflex.reverse { .flex.reverse {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.hflex.centered { .flex.vertical {
justify-content: center; flex-direction: column;
} }
.hint { .hint {
color: var(--color-table-gray); color: var(--color-table-gray);
@@ -587,10 +567,32 @@ form table select {
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;
} }
.vflex { .hmin50 {
display: flex; min-width: 50%;
gap: 0.8em; }
flex-direction: column; .hminnumber {
min-width: 5em;
}
.hmintext {
min-width: 6em;
}
.hstretch {
/* `width` conflicts with <input>/<textarea> size defined by [size]/[cols]
* attributes. It's best to not use these attributes (e.g. set them to 1) when
* setting width through stylesheet. */
/* NOTE: `field-sizing` can be considered after upgrade do FF >=152 */
width: -moz-available;
width: stretch;
}
.italic {
color: var(--color-gray);
font-style: italic;
}
.ralign {
text-align: right;
}
.rextend {
margin-right: auto;
} }
@@ -602,15 +604,15 @@ summary {
align-items: center; align-items: center;
color: var(--color-gray); color: var(--color-gray);
display: flex; display: flex;
gap: 0.2em; gap: 0.4em;
height: 100%; height: 100%;
white-space: nowrap; white-space: nowrap;
} }
summary::before { summary::before {
background-color: #000; background-color: currentColor;
content: ""; content: "";
height: 1em; height: 1em;
mask-image: url('pictograms/chevron-down.svg'); mask-image: url('chevron-down.svg');
mask-size: cover; mask-size: cover;
width: 1em; width: 1em;
} }
@@ -630,14 +632,14 @@ summary span {
width: 100%; width: 100%;
} }
details[open] summary::before { details[open] summary::before {
transform: rotate(180deg); transform: scaleY(-1);
} }
summary::marker { summary::marker {
padding-left: 0.25em; padding-left: 0.25em;
} }
/* NOTE: use `details[open]::details-content` once widely available. */ /* NOTE: use `details[open]::details-content` once widely available. */
details[open] ul { details[open] ul {
background: white; background-color: white;
border: 1px solid var(--color-border-gray); border: 1px solid var(--color-border-gray);
border-radius: 0.25em; border-radius: 0.25em;
box-shadow: 1px 1px 3px var(--color-border-gray); box-shadow: 1px 1px 3px var(--color-border-gray);
@@ -667,3 +669,8 @@ li::marker {
* * focused label styling (currently only checkbox has focus), * * focused label styling (currently only checkbox has focus),
* * disabled checkbox blue square focus removal. * * disabled checkbox blue square focus removal.
* */ * */
#measurement_form {
min-width: 66%;
width: max-content;
}
+3 -3
View File
@@ -55,9 +55,9 @@ class ApplicationController < ActionController::Base
private private
def render_no_content(record) def render_errors(record)
helpers.render_errors(record) target = record.new_record? ? [:new, record.parent || record.class] : [:edit, record]
render html: nil, layout: true render 'errors', assigns: {record: record, target: target}
end end
def rescue_turbo(exception) def rescue_turbo(exception)
+3 -3
View File
@@ -17,7 +17,7 @@ class Default::UnitsController < ApplicationController
def import def import
@unit.port!(current_user) @unit.port!(current_user)
flash.now[:notice] = t('.success', unit: @unit) flash.now.notice = t('.success', unit: @unit)
ensure ensure
run_and_render :index run_and_render :index
end end
@@ -30,14 +30,14 @@ class Default::UnitsController < ApplicationController
def export def export
@unit.port!(nil) @unit.port!(nil)
flash.now[:notice] = t('.success', unit: @unit) flash.now.notice = t('.success', unit: @unit)
ensure ensure
run_and_render :index run_and_render :index
end end
def destroy def destroy
@unit.destroy! @unit.destroy!
flash.now[:notice] = t('.success', unit: @unit) flash.now.notice = t('.success', unit: @unit)
ensure ensure
run_and_render :index run_and_render :index
end end
+13 -9
View File
@@ -19,9 +19,9 @@ class QuantitiesController < ApplicationController
def create def create
@quantity = current_user.quantities.new(quantity_params) @quantity = current_user.quantities.new(quantity_params)
if @quantity.save if @quantity.save
@before = @quantity.successive @before = @quantity.succeeding
@ancestors = @quantity.ancestors @ancestors = @quantity.ancestors
flash.now[:notice] = t('.success', quantity: @quantity) flash.now.notice = t('.success', quantity: @quantity)
else else
render :new render :new
end end
@@ -33,7 +33,7 @@ class QuantitiesController < ApplicationController
def update def update
if @quantity.update(quantity_params.except(:parent_id)) if @quantity.update(quantity_params.except(:parent_id))
@ancestors = @quantity.ancestors @ancestors = @quantity.ancestors
flash.now[:notice] = t('.success', quantity: @quantity) flash.now.notice = t('.success', quantity: @quantity)
else else
render :edit render :edit
end end
@@ -43,18 +43,22 @@ class QuantitiesController < ApplicationController
permitted = params.require(:quantity).permit(:parent_id) permitted = params.require(:quantity).permit(:parent_id)
@previous_ancestors = @quantity.ancestors @previous_ancestors = @quantity.ancestors
# Until UI blocks all disallowed reparents, render error messages if present if @quantity.update(permitted)
render_no_content(@quantity) unless @quantity.update(permitted)
@ancestors = @quantity.ancestors @ancestors = @quantity.ancestors
@self_and_progenies = @quantity.with_progenies # TODO: simplify into 1 query (see #succeeding comment)
@before = @self_and_progenies.last.successive @quantities = @quantity.with_progenies
@before = @quantities.last.succeeding
else
# Until UI blocks all disallowed reparents, render error messages if present.
flash.now.alert = @quantity.errors.full_messages
render :nothing
end
end end
def destroy def destroy
@quantity.destroy! @quantity.destroy!
@ancestors = @quantity.ancestors @ancestors = @quantity.ancestors
flash.now[:notice] = t('.success', quantity: @quantity) flash.now.notice = t('.success', quantity: @quantity)
end end
private private
+1 -1
View File
@@ -6,7 +6,7 @@ class ReadoutsController < ApplicationController
def new def new
@quantities -= @prev_quantities @quantities -= @prev_quantities
# TODO: raise ParameterInvalid if new_quantities.empty? # TODO: raise ParameterInvalid if new_quantities.empty?
@readouts = current_user.readouts.build(@quantities.map { |q| {quantity: q} }) @readouts = @quantities.map { |q| q.readouts.build }
@user_units = current_user.units.ordered @user_units = current_user.units.ordered
+17 -26
View File
@@ -1,10 +1,9 @@
class UnitsController < ApplicationController class UnitsController < ApplicationController
before_action only: :new do before_action ->{ @unit = current_user.units.find_by!(id: params[:id]) },
find_unit if params[:id].present? only: [:new, :edit, :update, :rebase, :destroy],
end unless: ->{ action_name == "new" && params[:id].nil? }
before_action :find_unit, only: [:edit, :update, :rebase, :destroy]
before_action except: :index do before_action except: [:index] do
raise AccessForbidden unless current_user.at_least(:active) raise AccessForbidden unless current_user.at_least(:active)
end end
@@ -17,12 +16,12 @@ class UnitsController < ApplicationController
end end
def create def create
@unit = current_user.units.new(unit_params) @unit = current_user.units.new(params.expect(Unit::ATTRIBUTES))
if @unit.save if @unit.save
@before = @unit.successive @before = @unit.succeeding
flash.now[:notice] = t('.success', unit: @unit) flash.now.notice = t('.success', unit: @unit)
else else
render :new render_errors @unit
end end
end end
@@ -30,40 +29,32 @@ class UnitsController < ApplicationController
end end
def update def update
if @unit.update(unit_params.except(:base_id)) if @unit.update(params.except(:base_id).expect(Unit::ATTRIBUTES))
flash.now[:notice] = t('.success', unit: @unit) flash.now.notice = t('.success', unit: @unit)
else else
render :edit render_errors @unit
end end
end end
# TODO: Avoid double table width change by first un-hiding table header, # TODO: Avoid double table width change by first un-hiding table header,
# then displaying index, e.g. by re-displaying header in index # then displaying index, e.g. by re-displaying header in index
def rebase def rebase
permitted = params.require(:unit).permit(:base_id) unit_params = params.expect(unit: :base_id)
permitted.merge!(multiplier: 1) if permitted[:base_id].blank? && @unit.multiplier != 1 unit_params.merge!(multiplier: 1.0) if unit_params[:base_id].blank?
@previous_base = @unit.base @previous_base = @unit.base
@unit.update!(permitted) @unit.update!(unit_params)
@before = @unit.successive @before = @unit.succeeding
if @unit.multiplier_previously_changed? if @unit.multiplier_previously_changed?
flash.now[:notice] = t(".multiplier_reset", unit: @unit) flash.now.notice = t(".multiplier_reset", unit: @unit)
end end
end end
def destroy def destroy
@unit.destroy! @unit.destroy!
flash.now[:notice] = t('.success', unit: @unit) flash.now.notice = t('.success', unit: @unit)
end end
private private
def unit_params
params.require(:unit).permit(Unit::ATTRIBUTES)
end
def find_unit
@unit = current_user.units.find_by!(id: params[:id])
end
end end
+24 -37
View File
@@ -40,7 +40,7 @@ module ApplicationHelper
# controller.action.* => helpers.label.model.* => activerecord.attributes.model.* # controller.action.* => helpers.label.model.* => activerecord.attributes.model.*
# First 2 levels are translated recursively. # First 2 levels are translated recursively.
label(method, class: classes) do |builder| label(method, class: classes) do |builder|
translation = I18n.config.with(**handler) { deep_translate(method, **options) } translation = I18n.with(**handler) { deep_translate(method, **options) }
translation.presence || "#{builder.translation}:" translation.presence || "#{builder.translation}:"
end end
end end
@@ -91,24 +91,25 @@ module ApplicationHelper
[:text_field, :password_field, :text_area].each do |selector| [:text_field, :password_field, :text_area].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 = {})
options[:maxlength] ||= object.class.type_for_attribute(method).limit options[:maxlength] ||= object.type_for_attribute(method).limit
if object.errors.include?(method) # If [cols]/[size] is not set, minimum field width is controlled with
options[:pattern] = except_pattern(object.public_send(method), options[:pattern]) # stylesheet. To prevent browser estimating missing [cols]/[size]
end # based on e.g. [maxlength], set it to 1.
options[:#{selector == :text_area ? :cols : :size}] ||= 1
super super
end end
RUBY_EVAL RUBY_EVAL
end end
def number_field(method, options = {}) def number_field(method, options = {})
attr_type = object.type_for_attribute(method) case object.type_for_attribute(method).type
if attr_type.type == :decimal when :float, :double
options[:value] = object.public_send(method)&.to_scientific options[:min] ||= Float::MIN_15.to_scientific
options[:step] ||= BigDecimal(10).power(-attr_type.scale) options[:max] ||= Float::MAX_15.to_scientific
options[:max] ||= BigDecimal(10).power(attr_type.precision - attr_type.scale) - options[:oninput] = 'numberValidate(event)'
options[:step] options[:size] ||= options[:class]&.split&.include?('hstretch') ? 1 : 6
options[:min] = options[:min] == :step ? options[:step] : options[:min] options[:step] ||= :any
options[:min] ||= -options[:max] options[:value] = object.public_send("#{method}_before_type_cast")
end end
super super
end end
@@ -127,11 +128,7 @@ module ApplicationHelper
def submit_default_value def submit_default_value
svg_name = object ? (object.persisted? ? 'update' : 'plus-circle-outline') : '' svg_name = object ? (object.persisted? ? 'update' : 'plus-circle-outline') : ''
@template.svg_tag("pictograms/#{svg_name}", super) @template.svg_tag(svg_name, super)
end
def except_pattern(value, pattern = nil)
"(?!^#{Regexp.escape(value)}$)" + (pattern || ".*")
end end
end end
@@ -142,20 +139,19 @@ module ApplicationHelper
record_object, options = nil, record_object if record_object.is_a?(Hash) record_object, options = nil, record_object if record_object.is_a?(Hash)
extra_options = {builder: TabularFormBuilder, skip_default_ids: true} extra_options = {builder: TabularFormBuilder, skip_default_ids: true}
options = merge_attributes(options, extra_options) options = merge_attributes(options, extra_options)
# TODO: set error message with setCustomValidity instead of rendering to flash?
render_errors(record_object || record_name)
fields_for(record_name, record_object, **options, &block) fields_for(record_name, record_object, **options, &block)
end end
def tabular_form_with(**options, &block) def tabular_form_with(**options, &block)
extra_options = {builder: TabularFormBuilder, html: {autocomplete: 'off'}} extra_options = {builder: TabularFormBuilder, class: 'tabular-form',
html: {autocomplete: 'off'}}
form_with(**merge_attributes(options, extra_options), &block) form_with(**merge_attributes(options, extra_options), &block)
end end
def svg_tag(source, label = nil, options = {}) def svg_tag(source, label = nil, options = {})
label, options = nil, label if label.is_a? Hash label, options = nil, label if label.is_a? Hash
svg_tag = 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)
end end
@@ -171,7 +167,7 @@ module ApplicationHelper
menu_tabs.map do |name, image, status, css_class| menu_tabs.map do |name, image, status, css_class|
if current_user.at_least(status) if current_user.at_least(status)
link_to svg_tag("pictograms/#{image}", t("#{name}.navigation")), link_to svg_tag(image, t("#{name}.navigation")),
{controller: "/#{name}", action: "index"}, {controller: "/#{name}", action: "index"},
class: class_names('tab', css_class, active: name == current_tab) class: class_names('tab', css_class, active: name == current_tab)
end end
@@ -203,28 +199,19 @@ 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)
# NOTE: Starting from Rails 8.1.0, below condition can be replaced with: if current_page?(options, method: [:get, :post])
# current_page?(options, method: [:get, :post])
if request.path == url_for(options)
html_options = html_options.deep_merge DISABLED_ATTRIBUTES html_options = html_options.deep_merge DISABLED_ATTRIBUTES
end end
link_to name, options, html_options link_to name, options, html_options
end end
def render_errors(records) def render_flash
# Conversion of flash to Array only required because of Devise
# TODO: override Devise message setting to Array()?
flash[:alert] = Array(flash[:alert])
Array(records).each { |record| flash[:alert] += record.errors.full_messages }
end
def render_flash_messages
flash.map do |entry, messages| flash.map do |entry, messages|
# Conversion of flash to Array only required because of Devise # 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.span(sanitize(message)) + tag.span(sanitize(message)) +
svg_tag('pictograms/close-outline', {onclick: "this.parentElement.remove()"}) svg_tag('close-outline', {onclick: "this.parentElement.remove()"})
end end
end end
end.join.html_safe end.join.html_safe
@@ -256,7 +243,7 @@ module ApplicationHelper
link_is_local = html_options[:onclick] || html_options.dig(:data, :turbo_stream) link_is_local = html_options[:onclick] || html_options.dig(:data, :turbo_stream)
name = name.to_s name = name.to_s
name += '...' if type == :link && !link_is_local name += '...' if type == :link && !link_is_local
name = svg_tag("pictograms/#{image}", name) if image name = svg_tag(image, name) if image
[name, html_options] [name, html_options]
end end
+2 -2
View File
@@ -1,9 +1,9 @@
module QuantitiesHelper module QuantitiesHelper
def quantities_check_boxes def quantities_check_boxes(quantities)
# Closing <details> on focusout event depends on relatedTarget for internal # Closing <details> on focusout event depends on relatedTarget for internal
# actions being non-null. To ensure this, all top-layer elements of # actions being non-null. To ensure this, all top-layer elements of
# ::details-content must accept focus, e.g. <label> needs tabindex="-1" */ # ::details-content must accept focus, e.g. <label> needs tabindex="-1" */
collection_check_boxes(nil, :quantity, @quantities, :id, :to_s_with_depth, collection_check_boxes(nil, :quantity, quantities, :id, :to_s_with_depth,
include_hidden: false) do |b| include_hidden: false) do |b|
content_tag :li, b.label(tabindex: -1) { b.check_box + b.text } content_tag :li, b.label(tabindex: -1) { b.check_box + b.text }
end end
+36 -64
View File
@@ -1,14 +1,19 @@
// Configure your import map in config/importmap.rb. Read more: // Configure your import map in config/importmap.rb. Read more:
// https://github.com/rails/importmap-rails // https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails" import "@hotwired/turbo-rails"
import "./turbo_actions.js"
// TODO: access `data-` attributes through
// `keyname in`/`dataset.keyname`/`delete dataset.keyname`
/* Hide page before loaded for testing purposes */ // Show page if hidden for testing purposes.
function showPage(event) { function showPage(event) {
document.documentElement.style.visibility="visible" document.documentElement.style.visibility="visible"
} }
document.addEventListener('turbo:load', showPage) document.addEventListener('turbo:load', showPage)
// <details> event handlers.
function detailsChange(event) { function detailsChange(event) {
var target = event.currentTarget var target = event.currentTarget
var count = target.querySelectorAll('input:checked:not([disabled])').length var count = target.querySelectorAll('input:checked:not([disabled])').length
@@ -24,7 +29,7 @@ function detailsChange(event) {
} }
window.detailsChange = detailsChange window.detailsChange = detailsChange
/* Close open <details> when focus lost */ // Close open <details> when focus lost.
function detailsClose(event) { function detailsClose(event) {
if (!event.relatedTarget || if (!event.relatedTarget ||
event.relatedTarget.closest("details") != event.currentTarget) { event.relatedTarget.closest("details") != event.currentTarget) {
@@ -37,6 +42,8 @@ window.detailsObserver = new MutationObserver((mutations) => {
mutations[0].target.dispatchEvent(new Event('change', {bubbles: true})) mutations[0].target.dispatchEvent(new Event('change', {bubbles: true}))
}); });
// Validation of multiple-submit forms with different field requirements.
function formValidate(event) { function formValidate(event) {
var id = event.submitter.getAttribute("data-validate") var id = event.submitter.getAttribute("data-validate")
if (!id) return; if (!id) return;
@@ -49,74 +56,35 @@ function formValidate(event) {
} }
window.formValidate = formValidate window.formValidate = formValidate
// Pattern for number precision verification. Should accept all number formats
// accepted by Kernel#Float. Lookahead checks the number of significant digits
// in mantissa, following expression makes sure there is only one dot.
const numberRegExp = /^\-?(?=(?:\.?0)*(?:\.?\d){1,15}?(?:\.?0)*\.?(?:[eE]|$))(?<mantissa>\d*\.?\d+|\d+\.)(?:[eE]\-?\d{1,3})?$/
/* Turbo stream actions */ // Validation of input[type=number] value precision.
Turbo.StreamElement.prototype.disableElement = function(element) { function numberValidate(event) {
element.setAttribute("disabled", "disabled") var input = event.target
element.setAttribute("aria-disabled", "true")
element.setAttribute("tabindex", "-1")
}
Turbo.StreamActions.disable = function() {
this.targetElements.forEach((e) => { this.disableElement(e) })
}
Turbo.StreamElement.prototype.enableElement = function(element) { input.setCustomValidity("")
element.removeAttribute("disabled") if (!input.validity.valid) return
element.removeAttribute("aria-disabled")
// Assume 'tabindex' is not used explicitly, so removing it is safe
element.removeAttribute("tabindex")
}
Turbo.StreamActions.enable = function() {
this.targetElements.forEach((e) => { this.enableElement(e) })
}
/* TODO: change to visibility = collapse to avoid width change? */ var match = numberRegExp.exec(input.value)
Turbo.StreamActions.hide = function() { if (!match ||
this.targetElements.forEach((e) => { e.style.display = "none" }) (parseFloat(input.value) == 0.0 && match.groups['mantissa'].match(/[1-9]/))) {
// TODO: move message to locales
input.setCustomValidity("Please select a value within range of double precision floating-point format.")
} }
}
window.numberValidate = numberValidate
Turbo.StreamActions.show = function() {
this.targetElements.forEach((e) => { e.style.removeProperty("display") })
}
/*
Turbo.StreamActions.collapse = function() {
this.targetElements.forEach((e) => { e.style.visibility = "collapse" })
}
*/
Turbo.StreamActions.close_form = function() {
this.targetElements.forEach((e) => {
/* Move focus if there's no focus or focus inside form being closed */
const focused = document.activeElement
if (!focused || (focused == document.body) || e.contains(focused)) {
let nextForm = e.parentElement.querySelector(`#${e.id} ~ tr:has([autofocus])`)
nextForm ??= e.parentElement.querySelector("tr:has([autofocus])")
nextForm?.querySelector("[autofocus]").focus()
}
document.getElementById(e.getAttribute("data-form")).remove()
if (e.hasAttribute("data-link")) {
this.enableElement(document.getElementById(e.getAttribute("data-link")))
}
if (e.hasAttribute("data-hidden-row")) {
document.getElementById(e.getAttribute("data-hidden-row")).removeAttribute("style")
}
e.remove()
})
}
Turbo.StreamActions.unselect = function() {
this.targetElements.forEach((e) => {
e.checked = false
this.enableElement(e)
})
}
// Keyboard event processing handlers.
function formProcessKey(event) { function formProcessKey(event) {
switch (event.key) { switch (event.key) {
case "Escape": case "Escape":
event.currentTarget.querySelector("a[name=cancel]").click() event.currentTarget.querySelector("a[name=cancel]").click()
break break
// TODO: allow new line inside <textarea> (submit with ctrl-enter?)
case "Enter": case "Enter":
event.currentTarget.querySelector("button[name=button]").click() event.currentTarget.querySelector("button[name=button]").click()
event.preventDefault() event.preventDefault()
@@ -138,7 +106,7 @@ function detailsProcessKey(event) {
var button = event.currentTarget.querySelector("button:not([disabled])") var button = event.currentTarget.querySelector("button:not([disabled])")
if (button) { if (button) {
button.click() button.click()
// Autofocus won't be respected unless target is blurred // Autofocus won't be respected unless target is blurred.
event.target.blur() event.target.blur()
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@@ -148,7 +116,8 @@ function detailsProcessKey(event) {
} }
window.detailsProcessKey = detailsProcessKey; window.detailsProcessKey = detailsProcessKey;
/* Items table drag and drop support */
// Items table drag and drop support.
var lastEnterTime var lastEnterTime
function dragStart(event) { function dragStart(event) {
lastEnterTime = event.timeStamp lastEnterTime = event.timeStamp
@@ -158,7 +127,8 @@ function dragStart(event) {
}) })
event.dataTransfer.setData("text/plain", row.getAttribute("data-drag-path")) event.dataTransfer.setData("text/plain", row.getAttribute("data-drag-path"))
var rowRectangle = row.getBoundingClientRect() var rowRectangle = row.getBoundingClientRect()
event.dataTransfer.setDragImage(row, event.x - rowRectangle.left, event.y - rowRectangle.top) event.dataTransfer.setDragImage(row, event.x - rowRectangle.left,
event.y - rowRectangle.top)
event.dataTransfer.dropEffect = "move" event.dataTransfer.dropEffect = "move"
} }
window.dragStart = dragStart window.dragStart = dragStart
@@ -168,9 +138,11 @@ window.dragStart = dragStart
* * Enter/Leave events at the same timeStamp may not be logically ordered * * Enter/Leave events at the same timeStamp may not be logically ordered
* (e.g. E -> E -> L, not E -> L -> E), * (e.g. E -> E -> L, not E -> L -> E),
* * not every Enter event has corresponding Leave event, especially during * * not every Enter event has corresponding Leave event, especially during
* rapid pointer moves * rapid pointer moves.
* NOTE: sometimes Leave is not emitted when pointer goes fast over table * NOTE: sometimes Leave is not emitted when pointer goes fast over table
* and outside. This should probably be fixed in browser, than patched here. * and outside. This should probably be fixed in browser, than patched here.
* TODO: replace Leave table with Enter document?
* https://www.quirksmode.org/js/events_order.html#link4
*/ */
function dragEnter(event) { function dragEnter(event) {
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id) //console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id)
@@ -188,7 +160,7 @@ window.dragOver = dragOver
function dragLeave(event) { function dragLeave(event) {
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id) //console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id)
// Leave has been accounted for by Enter at the same timestamp, processed earlier // Leave has been accounted for by Enter at the same timestamp, processed earlier.
if (event.timeStamp <= lastEnterTime) return if (event.timeStamp <= lastEnterTime) return
event.currentTarget.closest("table").querySelectorAll(".dropzone").forEach((tr) => { event.currentTarget.closest("table").querySelectorAll(".dropzone").forEach((tr) => {
tr.classList.remove("dropzone") tr.classList.remove("dropzone")
+97
View File
@@ -0,0 +1,97 @@
// Turbo Streams actions.
const FORM_CONTROLS = ["BUTTON", "FIELDSET", "INPUT", "OPTGROUP", "OPTION", "SELECT",
"TEXTAREA"]
Object.assign(Turbo.StreamElement.prototype, {
disableElement(element) {
if (FORM_CONTROLS.includes(element.tagName)) {
element.setAttribute("disabled", "disabled")
} else {
element.setAttribute("aria-disabled", "true")
// NOTE: tabindex should not be disabled?
element.setAttribute("tabindex", "-1")
}
},
enableElement(element) {
if (FORM_CONTROLS.includes(element.tagName)) {
element.removeAttribute("disabled")
} else {
element.removeAttribute("aria-disabled")
// Assume 'tabindex' is not used explicitly, so removing it is safe.
element.removeAttribute("tabindex")
}
},
showElement(element) { element.style.visibility = "visible" }
})
Object.assign(Turbo.StreamActions, {
disable() {
this.targetElements.forEach((e) => { this.disableElement(e) })
},
enable() {
this.targetElements.forEach((e) => { this.enableElement(e) })
},
hide() {
this.targetElements.forEach((e) => { e.style.visibility = "collapse" })
},
show() {
this.targetElements.forEach((e) => { this.showElement(e) })
},
unselect() {
this.targetElements.forEach((e) => { e.checked = false; this.enableElement(e) })
},
validate_form() {
this.targetElements.forEach((form) => {
this.templateContent.querySelectorAll("span").forEach((span) => {
var control = document.querySelector(
`[form="${form.id}"][name="${span.dataset.name}"]`
)
control.setCustomValidity(span.textContent)
// Add event listener in capture phase to clear custom validity before
// `[oninput]` or any other bubble phase handler is invoked.
// https://www.quirksmode.org/js/events_order.html
// TODO: keep invalidating current value whenever user re-enters it. Store
// invalid value and validation message in `[data-invalid-message/value]`
// and check input value on "input" event (below) (using `[pattern]` may not
// be suitable due to inability to customize validtion message; also
// pattern may be used for other, conflicting purposes).
// After user executes any other action (while form is open), remove `[data-]`
// attributes and custom validation, as value may no longer be invalid.
control.addEventListener(
"input",
function(event) { event.target.setCustomValidity("") },
{capture: true, once: true}
)
})
form.reportValidity()
})
},
close_form() {
this.targetElements.forEach((e) => {
// Move focus if there's no focus or focus inside form being closed.
const focused = document.activeElement
if (!focused || (focused == document.body) || e.contains(focused)) {
let nextForm = e.parentElement.querySelector(`#${e.id} ~ tr:has([autofocus])`)
nextForm ??= e.parentElement.querySelector("tr:has([autofocus])")
nextForm?.querySelector("[autofocus]").focus()
}
document.getElementById(e.dataset.form).remove()
if ('link' in e.dataset) {
this.enableElement(document.getElementById(e.dataset.link))
}
if ('hiddenRow' in e.dataset) {
this.showElement(document.getElementById(e.dataset.hiddenRow))
}
e.remove()
})
}
})
+2
View File
@@ -1,4 +1,6 @@
class ApplicationRecord < ActiveRecord::Base class ApplicationRecord < ActiveRecord::Base
include ActionView::Helpers::TagHelper
class << self class << self
# Cached attribute has non-user assignable value calculated from other # Cached attribute has non-user assignable value calculated from other
# attributes' values on create/update. This simplifies and speeds up # attributes' values on create/update. This simplifies and speeds up
+3
View File
@@ -0,0 +1,3 @@
class Note < ApplicationRecord
ATTRIBUTES = [:text]
end
+45 -22
View File
@@ -6,16 +6,21 @@ class Quantity < ApplicationRecord
belongs_to :parent, optional: true, class_name: "Quantity" belongs_to :parent, optional: true, class_name: "Quantity"
has_many :subquantities, ->{ order(:name) }, class_name: "Quantity", has_many :subquantities, ->{ order(:name) }, class_name: "Quantity",
inverse_of: :parent, dependent: :restrict_with_error inverse_of: :parent, dependent: :restrict_with_error
has_many :readouts, dependent: :restrict_with_error
validate if: ->{ parent.present? } do validate if: ->{ parent.present? } do
errors.add(:parent, :user_mismatch) unless user_id == parent.user_id errors.add(:parent, :user_mismatch) unless user_id == parent.user_id
errors.add(:parent, :self_reference) if id == parent_id errors.add(:parent, :self_reference) if id == parent_id
end end
validate if: ->{ parent.present? }, on: :update do validate if: ->{ parent.present? }, on: :update do
# TODO: should not report error for (parent == self)
errors.add(:parent, :descendant_reference) if ancestor_of?(parent) errors.add(:parent, :descendant_reference) if ancestor_of?(parent)
end end
validates :name, presence: true, uniqueness: {scope: [:user_id, :parent_id]}, validates :name, presence: true, uniqueness: {scope: [:user_id, :parent_id]},
length: {maximum: type_for_attribute(:name).limit} length: {maximum: type_for_attribute(:name).limit}
# FIXME: length validation for :text type column is probably wrong; reported
# limit is in bytes (at least for MySQL) and check is for #length (i.e.
# characters); this doesn't seem to apply to :string (implemented as varchar)
validates :description, length: {maximum: type_for_attribute(:description).limit} validates :description, length: {maximum: type_for_attribute(:description).limit}
# Update :depths of progenies after parent change # Update :depths of progenies after parent change
@@ -60,26 +65,42 @@ class Quantity < ApplicationRecord
scope :defaults, ->{ where(user: nil) } scope :defaults, ->{ where(user: nil) }
# Return: ordered [sub]hierarchy # Return: ordered [sub]hierarchy
scope :ordered, ->(root: nil, include_root: true) { scope :ordered, ->(**kwargs) {
numbered = Arel::Table.new('numbered') with_path(**kwargs).order(arel_table[:path])
}
self.model.with(numbered: numbered(:parent_id, :name)).with_recursive(arel_table.name => [ # TODO: sort recursive CTE using ORDER BY; it's supported by SQLite, so
numbered.project( # drop MySQL until it is available (?)
numbered[Arel.star], # Only ORDER BY allows proper i.e. not placing limits on user data and
numbered.cast(numbered[:child_number], 'BINARY').as('path') # efficient with regard to paging (LIMIT/OFFSET) depth-first search.
).where(numbered[root && include_root ? :id : :parent_id].eq(root)), # Sorting based on numbering is less elegant/efficient (MySQL: ROW_NUMBER cannot
numbered.project( # be used inside recursive CTE and needs separate SELECT,
numbered[Arel.star], # https://bugs.mysql.com/bug.php?id=96538).
arel_table[:path].concat(numbered[:child_number]) # Sorting based on pathname requires I) padding to maximum column length or
).join(arel_table).on(numbered[:parent_id].eq(arel_table[:id])) # II) delimiter which a) cannot be used in sorted column and b) sorts before
]).order(arel_table[:path]) # anything else (this adds complexity; NULL cannot be used when sorting).
# Sorting based on ARRAY is only available on PostgreSQL.
scope :with_path, ->(root: nil, include_root: true) {
with_number = Arel::Table.new('with_number')
self.model.with(with_number: with_number(:parent_id, :name))
.with_recursive(arel_table.name => [
with_number.project(
with_number[Arel.star],
with_number.cast(with_number[:number], 'BINARY').as('path')
).where(with_number[root && include_root ? :id : :parent_id].eq(root)),
with_number.project(
with_number[Arel.star],
arel_table[:path].concat(with_number[:number])
).join(arel_table).on(with_number[:parent_id].eq(arel_table[:id]))
])
} }
# TODO: extract named functions to custom Arel extension # TODO: extract named functions to custom Arel extension
# NOTE: once recursive queries allow use of window functions, this scope can # NOTE: once recursive queries allow use of window functions, this scope can
# be merged with :ordered # be merged with :ordered
# https://gist.github.com/ProGM/c6df08da14708dcc28b5ca325df37ceb#extending-arel # https://gist.github.com/ProGM/c6df08da14708dcc28b5ca325df37ceb#extending-arel
scope :numbered, ->(parent_column, order_column) { scope :with_number, ->(parent_column, order_column) {
select( select(
arel_table[Arel.star], arel_table[Arel.star],
Arel::Nodes::NamedFunction.new( Arel::Nodes::NamedFunction.new(
@@ -92,12 +113,12 @@ class Quantity < ApplicationRecord
).from(arel_table), ).from(arel_table),
Arel::Nodes.build_quoted('0') Arel::Nodes.build_quoted('0')
], ],
).as('child_number') ).as('number')
) )
} }
def to_s def to_s
name tag.pre name
end end
def to_s_with_depth def to_s_with_depth
@@ -130,15 +151,17 @@ class Quantity < ApplicationRecord
.order(arel_table[:depth].desc) .order(arel_table[:depth].desc)
} }
# Return: successive record in order of appearance; used for partial view reload # Return: succeeding record in order of appearance, used for partial view reload.
def successive # TODO: add scope returning succeeding element for multiple records
def succeeding
quantities = Quantity.arel_table quantities = Quantity.arel_table
Quantity.with( Quantity.with(lagging_quantities: user.quantities.with_path.select(
quantities: user.quantities.ordered.select(
quantities[Arel.star], quantities[Arel.star],
Arel::Nodes::NamedFunction.new('LAG', [quantities[:id]]).over.as('lag_id') # For SQLite 'OVER () ORDER BY x' is not equivalent to 'OVER (ORDER BY x)'.
) Arel::Nodes::NamedFunction.new('LAG', [quantities[:id]])
).where(quantities[:lag_id].eq(id)).first .over(Arel::Nodes::Window.new.order(Quantity.ordered.order_values)).as('lag_id')
)).from(Arel::Table.new(:lagging_quantities).as(:quantities))
.where(quantities[:lag_id].eq(id)).take
end end
def with_progenies def with_progenies
+2
View File
@@ -4,4 +4,6 @@ class Readout < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :quantity belongs_to :quantity
belongs_to :unit belongs_to :unit
# TODO: validate quantity.user_id == unit.user_id != NULL
end end
+21 -21
View File
@@ -1,8 +1,10 @@
class Unit < ApplicationRecord class Unit < ApplicationRecord
ATTRIBUTES = [:symbol, :description, :multiplier, :base_id] ATTRIBUTES = {unit: [:symbol, :description, :multiplier, :base_id]}
belongs_to :user, optional: true belongs_to :user, optional: true
belongs_to :base, optional: true, class_name: "Unit" belongs_to :base, optional: true, class_name: "Unit"
# TODO: use #alias_attribute after Rails upgrade?
alias_method :parent, :base
has_many :subunits, class_name: "Unit", inverse_of: :base, has_many :subunits, class_name: "Unit", inverse_of: :base,
dependent: :restrict_with_error dependent: :restrict_with_error
@@ -14,8 +16,8 @@ class Unit < ApplicationRecord
validates :symbol, presence: true, uniqueness: {scope: :user_id}, validates :symbol, presence: true, uniqueness: {scope: :user_id},
length: {maximum: type_for_attribute(:symbol).limit} length: {maximum: type_for_attribute(:symbol).limit}
validates :description, length: {maximum: type_for_attribute(:description).limit} validates :description, length: {maximum: type_for_attribute(:description).limit}
validates :multiplier, numericality: {equal_to: 1}, unless: :base validates :multiplier, numericality: {equal_to: 1.0}, unless: :base
validates :multiplier, numericality: {greater_than: 0, precision: true, scale: true}, if: :base validates :multiplier, numericality: {greater_than: 0.0}, if: :base
scope :defaults, ->{ where(user: nil) } scope :defaults, ->{ where(user: nil) }
scope :defaults_diff, ->{ scope :defaults_diff, ->{
@@ -77,15 +79,13 @@ class Unit < ApplicationRecord
.from(units).group(:base_id, :symbol) .from(units).group(:base_id, :symbol)
} }
scope :ordered, ->{ scope :ordered, ->{
left_outer_joins(:base).order(ordering) left_outer_joins(:base).order([
} arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]),
def self.ordering
[arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]),
arel_table[:base_id].not_eq(nil), arel_table[:base_id].not_eq(nil),
:multiplier, arel_table[:multiplier],
:symbol] arel_table[:symbol]
end ])
}
before_destroy do before_destroy do
# TODO: disallow destruction if any object depends on this unit # TODO: disallow destruction if any object depends on this unit
@@ -93,7 +93,7 @@ class Unit < ApplicationRecord
end end
def to_s def to_s
symbol tag.pre symbol
end end
def movable? def movable?
@@ -104,21 +104,21 @@ class Unit < ApplicationRecord
user_id.nil? user_id.nil?
end end
# Should only by invoked on Units returned from #defaults_diff which are #portable # Should only by invoked on Units returned from #defaults_diff which are #portable.
def port!(recipient) def port!(recipient)
recipient_base = base && Unit.find_by!(symbol: base.symbol, user: recipient) recipient_base = base && Unit.find_by!(symbol: base.symbol, user: recipient)
params = slice(ATTRIBUTES - [:symbol, :base_id]) params = slice(ATTRIBUTES[:unit] - [:symbol, :base_id])
Unit.find_or_initialize_by(user: recipient, symbol: symbol) Unit.find_or_initialize_by(user: recipient, symbol: symbol)
.update!(base: recipient_base, **params) .update!(base: recipient_base, **params)
end end
def successive def succeeding
units = Unit.arel_table units = Unit.arel_table
lead = Arel::Nodes::NamedFunction.new('LAG', [units[:id]]) Unit.with(lagging_units: user.units.left_outer_joins(:base).select(
window = Arel::Nodes::Window.new.order(*Unit.ordering) units[Arel.star],
lag_id = lead.over(window).as('lag_id') Arel::Nodes::NamedFunction.new('LAG', [units[:id]])
Unit.with( .over(Arel::Nodes::Window.new.order(Unit.ordered.order_values)).as('lag_id')
units: user.units.left_outer_joins(:base).select(units[Arel.star], lag_id) )).from(Arel::Table.new(:lagging_units).as(:units))
).where(units[:lag_id].eq(id)).first .where(units[:lag_id].eq(id)).take
end end
end end
@@ -0,0 +1,7 @@
<%= turbo_stream.validate_form dom_target(*@target, :form) do %>
<% @record.errors.attribute_names.map do |attr| %>
<%= tag.span data: {name: field_name(dom_class(@record), attr)} do %>
<%- @record.errors.full_messages_for(attr).map { |m| m + '.' }.join(' ') %>
<% end %>
<% end %>
<% end %>
+1 -1
View File
@@ -5,7 +5,7 @@
</td> </td>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<td class="actions"> <td class="flex">
<% unless unit.portable.nil? %> <% unless unit.portable.nil? %>
<% if unit.default? %> <% if unit.default? %>
<%= image_button_to_if unit.portable?, t('.import'), 'download-outline', <%= image_button_to_if unit.portable?, t('.import'), 'download-outline',
+1 -1
View File
@@ -8,7 +8,7 @@
class: 'tools-area' %> class: 'tools-area' %>
</div> </div>
<table class="main-area items"> <table class="main-area items-table">
<thead> <thead>
<tr> <tr>
<th><%= Unit.human_attribute_name(:symbol) %></th> <th><%= Unit.human_attribute_name(:symbol) %></th>
+3 -3
View File
@@ -23,10 +23,10 @@
</head> </head>
<body> <body>
<header class="hflex"> <header class="flex">
<%= image_link_to t(".source_code"), "code-braces", source_code_url %> <%= image_link_to t(".source_code"), "code-braces", source_code_url %>
<%= image_link_to t(".issue_tracker"), "bug-outline", issue_tracker_url, <%= image_link_to t(".issue_tracker"), "bug-outline", issue_tracker_url,
class: "extendedright" %> class: "rextend" %>
<% if user_signed_in? %> <% if user_signed_in? %>
<%= image_link_to_unless_current(current_user, "account-wrench-outline", <%= image_link_to_unless_current(current_user, "account-wrench-outline",
edit_user_registration_path) %> edit_user_registration_path) %>
@@ -44,7 +44,7 @@
</header> </header>
<div id="flashes"> <div id="flashes">
<%= render_flash_messages %> <%= render_flash %>
</div> </div>
<%# Allows overwriting/clearing navigation menu for some views %> <%# Allows overwriting/clearing navigation menu for some views %>
@@ -1,6 +1,6 @@
<%= yield %> <%# Render unconditionally, to clear previous flash messages. %>
<%# Some views may convert ActiveRecord errors to flashes, render at the end. %>
<%= turbo_stream.update :flashes do %> <%= turbo_stream.update :flashes do %>
<%= render_flash_messages %> <%= render_flash %>
<% end %> <% end %>
<%= yield %>
+22 -12
View File
@@ -1,15 +1,26 @@
<%= tabular_form_with model: Measurement.new, id: :measurement_form, <%= tabular_form_with model: Measurement.new, id: :measurement_form,
class: 'topside-area vflex', html: {onkeydown: 'formProcessKey(event)'} do |form| %> class: 'topside-area flex vertical center',
<table class="items centered"> html: {onkeydown: 'formProcessKey(event)'} do |form| %>
<tbody id="readouts"></tbody>
<table class="items-table center">
<tbody id="readouts">
<%= tabular_fields_for @measurement do |form| %>
<tr class="italic">
<td class="hexpand hmin50"><%= t '.taken_at_html' %></td>
<td colspan="3" class="ralign">
<%= form.datetime_field :taken_at, required: true %>
</td>
</tr>
<% end %>
</tbody>
</table> </table>
<div class="hflex"> <%# TODO: right-click selection; unnecessary with hierarchical tags? %>
<%# TODO: right-click selection %> <details id="quantity_select" class="center hexpand" open
<details id="quantity_select" class="hexpand" open
onkeydown="detailsProcessKey(event)"> onkeydown="detailsProcessKey(event)">
<summary autofocus> <summary autofocus>
<!-- TODO: Set content with CSS when span empty to avoid duplication --> <%# TODO: Set content with CSS when span empty to avoid duplication; or
change to disabled input with prompt %>
<span data-prompt="<%= t('.select_quantity') %>"> <span data-prompt="<%= t('.select_quantity') %>">
<%= t('.select_quantity') %> <%= t('.select_quantity') %>
</span> </span>
@@ -17,14 +28,13 @@
formaction: new_readout_path, formmethod: :get, formnovalidate: true, formaction: new_readout_path, formmethod: :get, formnovalidate: true,
data: {turbo_stream: true} %> data: {turbo_stream: true} %>
</summary> </summary>
<ul><%= quantities_check_boxes %></ul> <ul><%= quantities_check_boxes(@quantities) %></ul>
</details> </details>
<%= form.button id: :create_measurement_button, disabled: true -%>
</div>
<div class="hflex reverse"> <div class="flex reverse">
<%= form.button id: :create_measurement_button, disabled: true -%>
<%= image_link_to t(:cancel), "close-outline", measurements_path, name: :cancel, <%= image_link_to t(:cancel), "close-outline", measurements_path, name: :cancel,
class: 'dangerous', onclick: render_turbo_stream('form_close') %> class: 'auxiliary dangerous', onclick: render_turbo_stream('form_close') %>
</div> </div>
<% end %> <% end %>
+7 -10
View File
@@ -3,17 +3,14 @@
data: {link: link, form: form_tag, hidden_row: hidden_row} do %> data: {link: link, form: form_tag, hidden_row: hidden_row} do %>
<td style="--depth:<%= @quantity.depth %>"> <td style="--depth:<%= @quantity.depth %>">
<%= form.text_field :name, required: true, autofocus: true, size: 20 %> <%= form.text_field :name, autofocus: true, class: 'hstretch', required: true %>
</td> <td><%= form.text_area :description, class: 'hstretch', escape: false, rows: 1 %>
<td>
<%= form.text_area :description, cols: 30, rows: 1, escape: false %>
</td>
<td class="actions"> <td class="flex">
<%= form.button %> <%= form.button %>
<%= image_link_to t(:cancel), "close-outline", quantities_path, class: 'dangerous', <%= image_link_to t(:cancel), "close-outline", quantities_path,
name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %> class: 'dangerous', name: :cancel,
</td> onclick: render_turbo_stream('form_close', {row: row}) %>
<td></td> <td>
<% end %> <% end %>
<% end %> <% end %>
+17 -17
View File
@@ -1,24 +1,24 @@
<%= tag.tr id: dom_id(quantity), <tr id="<%= dom_target(quantity) %>"
ondragstart: "dragStart(event)", ondragend: "dragEnd(event)", ondragstart="dragStart(event)" ondragend="dragEnd(event)"
ondragover: "dragOver(event)", ondrop: "drop(event)", ondragover="dragOver(event)" ondrop="drop(event)"
ondragenter: "dragEnter(event)", ondragleave: "dragLeave(event)", ondragenter="dragEnter(event)" ondragleave="dragLeave(event)"
data: {drag_path: reparent_quantity_path(quantity), drop_id: dom_id(quantity), data-drag-path="<%= reparent_quantity_path(quantity) %>"
drop_id_param: "quantity[parent_id]"} do %> data-drop-id="<%= dom_target(quantity) %>"
data-drop-id-param="<%= field_name(:quantity, :parent_id) %>">
<td class="link" style="--depth:<%= quantity.depth %>"> <td style="--depth:<%= quantity.depth %>">
<%= link_to quantity, edit_quantity_path(quantity), onclick: 'this.blur();', <%= link_to quantity, edit_quantity_path(quantity), class: 'link',
data: {turbo_stream: true} %> onclick: 'this.blur();', data: {turbo_stream: true} %>
</td> <td><%= quantity.description %>
<td><%= quantity.description %></td>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<td class="actions"> <td class="flex">
<%= image_link_to t('.new_subquantity'), 'plus-outline', new_quantity_path(quantity), <%= image_link_to t('.new_subquantity'), 'plus-outline',
id: dom_id(quantity, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %> new_quantity_path(quantity), id: dom_target(:new, quantity, :link),
onclick: 'this.blur();', data: {turbo_stream: true} %>
<%= image_button_to_if quantity.destroyable?, t('.destroy'), 'delete-outline', <%= image_button_to_if quantity.destroyable?, t('.destroy'), 'delete-outline',
quantity_path(quantity), method: :delete %> quantity_path(quantity), method: :delete %>
</td>
<td class="handle" draggable="true">&#x283F</td> <td class="handle" draggable="true">&#x283F
<% end %>
<% end %> <% end %>
+2 -4
View File
@@ -1,7 +1,5 @@
<%= turbo_stream.close_form dom_id(@quantity.parent || Quantity, :new) %> <%= turbo_stream.close_form dom_target(:new, @quantity.parent || :quantity) %>
<%= turbo_stream.remove :no_items %>
<% @ancestors.each do |ancestor| %> <% @ancestors.each do |ancestor| %>
<%= turbo_stream.replace ancestor %> <%= turbo_stream.replace ancestor %>
<% end %> <% end %>
<%= @before ? turbo_stream.before(@before, @quantity) : <%= turbo_stream.before @before || :endmark, @quantity %>
turbo_stream.append(:quantities, @quantity) %>
@@ -2,4 +2,3 @@
<%= turbo_stream.replace ancestor %> <%= turbo_stream.replace ancestor %>
<% end %> <% end %>
<%= turbo_stream.remove @quantity %> <%= turbo_stream.remove @quantity %>
<%= turbo_stream.append(:quantities, render_no_items) if current_user.quantities.empty? %>
+4 -5
View File
@@ -1,13 +1,12 @@
<% ids = {row: dom_id(@quantity, :edit), <% ids = {row: dom_target(:edit, @quantity),
hidden_row: dom_id(@quantity), hidden_row: dom_target(@quantity),
link: nil, link: nil,
form_tag: dom_id(@quantity, :edit, :form)} %> form_tag: dom_target(:edit, @quantity, :form)} %>
<%= turbo_stream.append :quantity_form do %> <%= turbo_stream.append_all 'body' do %>
<%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do %> <%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do %>
<% end %> <% end %>
<% end %> <% end %>
<%= turbo_stream.hide ids[:hidden_row] %> <%= turbo_stream.hide ids[:hidden_row] %>
<%= turbo_stream.remove ids[:row] %>
<%= turbo_stream.after @quantity, partial: 'form', locals: ids -%> <%= turbo_stream.after @quantity, partial: 'form', locals: ids -%>
+20 -22
View File
@@ -1,33 +1,31 @@
<div class="rightside-area 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: :new_quantity_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',
class: 'tools-area' %> default_quantities_path, class: 'tools-area' %>
</div> </div>
<%= tag.div class: 'main-area', id: :quantity_form %> <table class="main-area items-table">
<caption class="hint"><%= t '.no_items' %></caption>
<table class="main-area items"> <tbody>
<thead>
<tr> <tr>
<th><%= Quantity.human_attribute_name(:name) %></th> <th class="hmintext"><%= Quantity.human_attribute_name(:name) %>
<th><%= Quantity.human_attribute_name(:description) %></th> <th class="hexpand"><%= Quantity.human_attribute_name(:description) %>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<th><%= t :actions %></th> <th><%= t :actions %>
<th></th> <th>
<tr id="quantity_" hidden
ondragover="dragOver(event)" ondrop="drop(event)"
ondragenter="dragEnter(event)" ondragleave="dragLeave(event)"
data-drop-id="quantity_"
data-drop-id-param="<%= field_name(:quantity, :parent_id) %>">
<th colspan="4"><%= t '.top_level_drop' %>
<% end %> <% end %>
</tr>
<%= tag.tr id: "quantity_", hidden: true, <%= render @quantities %>
ondragover: "dragOver(event)", ondrop: "drop(event)", <tr id="endmark" hidden>
ondragenter: "dragEnter(event)", ondragleave: "dragLeave(event)",
data: {drop_id: "quantity_", drop_id_param: "quantity[parent_id]"} do %>
<th colspan="4"><%= t '.top_level_drop' %></th>
<% end %>
</thead>
<tbody id="quantities">
<%= render(@quantities) || render_no_items %>
</tbody>
</table> </table>
+6 -11
View File
@@ -1,20 +1,15 @@
<% dom_obj = @quantity.parent || @quantity %> <% target_id = dom_target(:new, @quantity.parent || :quantity) %>
<% ids = {row: dom_id(dom_obj, :new), <% ids = {row: target_id,
hidden_row: nil, hidden_row: nil,
link: dom_id(dom_obj, :new, :link), link: dom_target(target_id, :link),
form_tag: dom_id(dom_obj, :new, :form)} %> form_tag: dom_target(target_id, :form)} %>
<%= turbo_stream.disable ids[:link] -%> <%= turbo_stream.disable ids[:link] -%>
<%= turbo_stream.append :quantity_form do %> <%= turbo_stream.append_all 'body' do %>
<%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do |form| %> <%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do |form| %>
<%= form.hidden_field :parent_id if @quantity.parent_id? %> <%= form.hidden_field :parent_id if @quantity.parent_id? %>
<% end %> <% end %>
<% end %> <% end %>
<% if @quantity.parent_id? %> <%= turbo_stream.after @quantity.parent || :quantity_, partial: 'form', locals: ids %>
<%= turbo_stream.remove ids[:row] %>
<%= turbo_stream.after @quantity.parent, partial: 'form', locals: ids %>
<% else %>
<%= turbo_stream.prepend :quantities, partial: 'form', locals: ids %>
<% end %>
@@ -1,9 +1,7 @@
<% @self_and_progenies.each do |q| %>
<%= turbo_stream.remove q %>
<% end %>
<% @previous_ancestors.union(@ancestors).each do |ancestor| %> <% @previous_ancestors.union(@ancestors).each do |ancestor| %>
<%= turbo_stream.replace ancestor %> <%= turbo_stream.replace ancestor %>
<% end %> <% end %>
<% @self_and_progenies.each do |q| %> <% @quantities.each do |quantity| %>
<%= @before ? turbo_stream.before(@before, q) : turbo_stream.append(:quantities, q) %> <%= turbo_stream.remove quantity %>
<%= turbo_stream.before @before || :endmark, quantity %>
<% end %> <% end %>
+1 -1
View File
@@ -1,4 +1,4 @@
<%= turbo_stream.close_form dom_id(@quantity, :edit) %> <%= turbo_stream.close_form dom_target(:edit, @quantity) %>
<% @ancestors.push(@quantity).each do |ancestor| %> <% @ancestors.push(@quantity).each do |ancestor| %>
<%= turbo_stream.replace ancestor %> <%= turbo_stream.replace ancestor %>
<% end %> <% end %>
+15 -16
View File
@@ -1,25 +1,24 @@
<%# TODO: add readout reordering by dragging %> <%# TODO: add readout reordering by dragging %>
<%= tabular_fields_for 'readouts[]', readout do |form| %> <%= tabular_fields_for 'readouts[]', readout do |form| %>
<%- tag.tr id: dom_id(readout.quantity, :new, :readout) do %> <%- tag.tr id: dom_target(:new, readout.quantity, :readout) do %>
<td class="actions"> <td>
<%# TODO: add grayed readout index (in separate column?) %>
<%= readout.quantity.relative_pathname(@superquantity) %>
<%= form.hidden_field :quantity_id %>
</td>
<td>
<%= form.number_field :value, required: true, autofocus: readout_counter == 0 %>
</td>
<td>
<%= form.collection_select :unit_id, @user_units, :id,
->(u){ sanitize('&emsp;' * (u.base_id ? 1 : 0) + u.symbol) },
{prompt: '', disabled: '', selected: ''}, required: true %>
</td>
<td class="flex">
<%# TODO: change to _link_ after giving up displaying relative paths %> <%# TODO: change to _link_ after giving up displaying relative paths %>
<%= image_button_tag '', 'delete-outline', class: 'dangerous', name: nil, <%= image_button_tag '', 'delete-outline', class: 'dangerous', name: nil,
formaction: discard_readouts_path(readout.quantity), formaction: discard_readouts_path(readout.quantity),
formmethod: :get, formnovalidate: true, data: {turbo_stream: true} %> formmethod: :get, formnovalidate: true, data: {turbo_stream: true} %>
</td> </td>
<td>
<%= readout.quantity.relative_pathname(@superquantity) %>
</td>
<td>
<%= form.number_field :value, required: true,
size: readout.type_for_attribute(:value).precision / 2,
autofocus: readout_counter == 0 %>
</td>
<td>
<%= form.hidden_field :quantity_id %>
<%= form.collection_select :unit_id, @user_units, :id,
->(u){ sanitize('&emsp;' * (u.base_id ? 1 : 0) + u.symbol) },
{prompt: t('.select_unit'), disabled: '', selected: ''}, required: true %>
</td>
<% end %> <% end %>
<% end %> <% end %>
+1 -1
View File
@@ -1,7 +1,7 @@
<%= turbo_stream.update(:measurement_form_legend) { @superquantity&.pathname } %> <%= turbo_stream.update(:measurement_form_legend) { @superquantity&.pathname } %>
<% @prev_quantities.each do |pq| %> <% @prev_quantities.each do |pq| %>
<%= turbo_stream.update dom_id(pq, nil, :pathname) do %> <%= turbo_stream.update dom_target(pq, :pathname) do %>
<%= pq.relative_pathname(@superquantity) %> <%= pq.relative_pathname(@superquantity) %>
<% end %> <% end %>
<% end %> <% end %>
+2 -2
View File
@@ -1,4 +1,4 @@
<%= turbo_stream.disable :create_measurement_button if @prev_quantities.one? %> <%= turbo_stream.disable :create_measurement_button if @prev_quantities.one? %>
<%= turbo_stream.remove dom_id(@quantity, :new, :readout) %> <%= turbo_stream.remove dom_target(:new, @quantity, :readout) %>
<%= render partial: 'form_repath' %> <%= render partial: 'form_repath' %>
<%= turbo_stream.unselect dom_id(@quantity) %> <%= turbo_stream.unselect dom_target(@quantity) %>
+1 -1
View File
@@ -1,5 +1,5 @@
<% @readouts.each do |r| %> <% @readouts.each do |r| %>
<%= turbo_stream.disable dom_id(r.quantity) %> <%= turbo_stream.disable dom_target(r.quantity) %>
<% end %> <% end %>
<%= render partial: 'form_repath' %> <%= render partial: 'form_repath' %>
<%= turbo_stream.append :readouts do %> <%= turbo_stream.append :readouts do %>
+7 -10
View File
@@ -3,20 +3,17 @@
data: {link: link, form: form_tag, hidden_row: hidden_row} do %> data: {link: link, form: form_tag, hidden_row: hidden_row} do %>
<td style="--depth:<%= @unit.base_id? ? 1 : 0 %>"> <td style="--depth:<%= @unit.base_id? ? 1 : 0 %>">
<%= form.text_field :symbol, required: true, autofocus: true, size: 12 %> <%= form.text_field :symbol, autofocus: true, class: 'hstretch', required: true %>
</td>
<td> <td>
<%= form.text_area :description, cols: 30, rows: 1, escape: false %> <% if @unit.base_id? %>
</td> <%= form.number_field :multiplier, class: 'hstretch', required: true %>
<td class="number"> <% end %>
<%= form.number_field :multiplier, required: true, size: 10, min: :step if @unit.base_id? %> <td><%= form.text_area :description, class: 'hstretch', escape: false, rows: 1 %>
</td>
<td class="actions"> <td class="flex">
<%= form.button %> <%= form.button %>
<%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous', <%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous',
name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %> name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %>
</td> <td>
<td></td>
<% end %> <% end %>
<% end %> <% end %>
+21 -20
View File
@@ -1,31 +1,32 @@
<%= tag.tr id: dom_id(unit), <tr id="<%= dom_target(unit) %>"
ondragstart: "dragStart(event)", ondragend: "dragEnd(event)", ondragstart="dragStart(event)" ondragend="dragEnd(event)"
ondragover: "dragOver(event)", ondrop: "drop(event)", ondragover="dragOver(event)" ondrop="drop(event)"
ondragenter: "dragEnter(event)", ondragleave: "dragLeave(event)", ondragenter="dragEnter(event)" ondragleave="dragLeave(event)"
data: {drag_path: rebase_unit_path(unit), data-drag-path="<%= rebase_unit_path(unit) %>"
drop_id: dom_id(unit.base || unit), data-drop-id="<%= dom_target(unit.base || unit) %>"
drop_id_param: "unit[base_id]"} do %> data-drop-id-param="<%= field_name(:unit, :base_id) %>">
<td class="link" style="--depth:<%= unit.base_id? ? 1 : 0 %>"> <td style="--depth:<%= unit.base_id? ? 1 : 0 %>">
<%= link_to unit, edit_unit_path(unit), onclick: 'this.blur();', data: {turbo_stream: true} %> <%= link_to unit, edit_unit_path(unit), class: 'link', onclick: 'this.blur();',
</td> data: {turbo_stream: true} %>
<td><%= unit.description %></td> <%# Closing <td> tag required to avoid whitespace inclusion for testing. %>
<td class="number"><%= unit.multiplier.to_html %></td> <td class="ralign"><%= unit.multiplier.to_html if unit.base_id? %></td>
<td><%= unit.description %>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<td class="actions"> <td class="flex">
<% unless unit.base_id? %> <% unless unit.base_id? %>
<%= image_link_to t('.new_subunit'), 'plus-outline', new_unit_path(unit), <%= image_link_to t('.new_subunit'), 'plus-outline', new_unit_path(unit),
id: dom_id(unit, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %> id: dom_target(:new, unit, :link), onclick: 'this.blur();',
data: {turbo_stream: true} %>
<% end %> <% end %>
<%= image_button_to_if unit.movable?, t('.destroy'), 'delete-outline', unit_path(unit), <%= image_button_to_if unit.movable?, t('.destroy'), 'delete-outline',
method: :delete %> unit_path(unit), method: :delete %>
</td>
<% if unit.movable? %> <% if unit.movable? %>
<td class="handle" draggable="true">&#x283F</td> <td class="handle" draggable="true">&#x283F
<% else %> <% else %>
<td></td> <td>
<% end %>
<% end %> <% end %>
<% end %> <% end %>
+2 -3
View File
@@ -1,4 +1,3 @@
<%= turbo_stream.close_form dom_id(@unit.base || Unit, :new) %> <%= turbo_stream.close_form dom_target(:new, @unit.base || :unit) %>
<%= turbo_stream.remove :no_items %>
<%= turbo_stream.replace @unit.base if @unit.base_id? %> <%= turbo_stream.replace @unit.base if @unit.base_id? %>
<%= @before ? turbo_stream.before(@before, @unit) : turbo_stream.append(:units, @unit) %> <%= turbo_stream.before @before || :endmark, @unit %>
-1
View File
@@ -1,3 +1,2 @@
<%= turbo_stream.replace @unit.base if @unit.base_id? %> <%= turbo_stream.replace @unit.base if @unit.base_id? %>
<%= turbo_stream.remove @unit %> <%= turbo_stream.remove @unit %>
<%= turbo_stream.append(:units, render_no_items) if current_user.units.empty? %>
+4 -5
View File
@@ -1,13 +1,12 @@
<% ids = {row: dom_id(@unit, :edit), <% ids = {row: dom_target(:edit, @unit),
hidden_row: dom_id(@unit), hidden_row: dom_target(@unit),
link: nil, link: nil,
form_tag: dom_id(@unit, :edit, :form)} %> form_tag: dom_target(:edit, @unit, :form)} %>
<%= turbo_stream.append :unit_form do %> <%= turbo_stream.append_all 'body' do %>
<%- tabular_form_with model: @unit, html: {id: ids[:form_tag]} do %> <%- tabular_form_with model: @unit, html: {id: ids[:form_tag]} do %>
<% end %> <% end %>
<% end %> <% end %>
<%= turbo_stream.hide ids[:hidden_row] %> <%= turbo_stream.hide ids[:hidden_row] %>
<%= turbo_stream.remove ids[:row] %>
<%= turbo_stream.after @unit, partial: 'form', locals: ids -%> <%= turbo_stream.after @unit, partial: 'form', locals: ids -%>
+20 -20
View File
@@ -1,33 +1,33 @@
<div class="rightside-area 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: :new_unit_link, onclick: 'this.blur();', data: {turbo_stream: true} %>
<% end %> <% end %>
<%= image_link_to t('.import_units'), 'download-outline', default_units_path, <%= image_link_to t('.import_units'), 'download-outline', default_units_path,
class: 'tools-area' %> class: 'tools-area' %>
</div> </div>
<%= tag.div id: :unit_form %> <table class="main-area items-table">
<caption class="hint"><%= t '.no_items' %></caption>
<table class="main-area items"> <tbody>
<thead>
<tr> <tr>
<th><%= Unit.human_attribute_name(:symbol) %></th> <th class="hmintext"><%= Unit.human_attribute_name(:symbol) %>
<th><%= Unit.human_attribute_name(:description) %></th> <th class="hminnumber"><%= Unit.human_attribute_name(:multiplier) %>
<th><%= Unit.human_attribute_name(:multiplier) %></th> <th class="hexpand"><%= Unit.human_attribute_name(:description) %>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<th><%= t :actions %></th> <th><%= t :actions %>
<th></th> <th>
<tr id="unit_" hidden
ondragover="dragOver(event)" ondrop="drop(event)"
ondragenter="dragEnter(event)" ondragleave="dragLeave(event)"
data-drop-id="unit_" data-drop-id-param="<%= field_name(:unit, :base_id) %>">
<th colspan="5"><%= t '.top_level_drop' %>
<% end %> <% end %>
</tr>
<%= tag.tr id: "unit_", hidden: true, <%= render @units %>
ondragover: "dragOver(event)", ondrop: "drop(event)", <%# Hidden rows with IDs allow simplier row insertion by using only
ondragenter: "dragEnter(event)", ondragleave: "dragLeave(event)", turbo_stream #after/#before instead of #after + #prepend/#before + #append. %>
data: {drop_id: "unit_", drop_id_param: "unit[base_id]"} do %> <tr id="endmark" hidden>
<th colspan="5"><%= t '.top_level_drop' %></th>
<% end %>
</thead>
<tbody id="units">
<%= render(@units) || render_no_items %>
</tbody>
</table> </table>
+6 -11
View File
@@ -1,20 +1,15 @@
<% dom_obj = @unit.base || @unit %> <% target_id = dom_target(:new, @unit.base || :unit) %>
<% ids = {row: dom_id(dom_obj, :new), <% ids = {row: target_id,
hidden_row: nil, hidden_row: nil,
link: dom_id(dom_obj, :new, :link), link: dom_target(target_id, :link),
form_tag: dom_id(dom_obj, :new, :form)} %> form_tag: dom_target(target_id, :form)} %>
<%= turbo_stream.disable ids[:link] -%> <%= turbo_stream.disable ids[:link] -%>
<%= turbo_stream.append :unit_form do %> <%= turbo_stream.append_all 'body' do %>
<%- tabular_form_with model: @unit, html: {id: ids[:form_tag]} do |form| %> <%- tabular_form_with model: @unit, html: {id: ids[:form_tag]} do |form| %>
<%= form.hidden_field :base_id if @unit.base_id? %> <%= form.hidden_field :base_id if @unit.base_id? %>
<% end %> <% end %>
<% end %> <% end %>
<% if @unit.base_id? %> <%= turbo_stream.after @unit.base || :unit_, partial: 'form', locals: ids %>
<%= turbo_stream.remove ids[:row] %>
<%= turbo_stream.after @unit.base, partial: 'form', locals: ids %>
<% else %>
<%= turbo_stream.prepend :units, partial: 'form', locals: ids %>
<% end %>
+4 -3
View File
@@ -1,4 +1,5 @@
<% [@previous_base, @unit.base].uniq.compact.each do |unit| %>
<%= turbo_stream.replace unit %>
<% end %>
<%= turbo_stream.remove @unit %> <%= turbo_stream.remove @unit %>
<%= turbo_stream.replace @previous_base if @previous_base %> <%= turbo_stream.before @before || :endmark, @unit %>
<%= turbo_stream.replace @unit.base if @unit.base_id? && (@previous_base.id != @unit.base_id) %>
<%= @before ? turbo_stream.before(@before, @unit) : turbo_stream.append(:units, @unit) %>
+1 -1
View File
@@ -1,3 +1,3 @@
<%= turbo_stream.close_form dom_id(@unit, :edit) %> <%= turbo_stream.close_form dom_target(:edit, @unit) %>
<%= turbo_stream.replace @unit.base if @unit.base_id? %> <%= turbo_stream.replace @unit.base if @unit.base_id? %>
<%= turbo_stream.replace @unit %> <%= turbo_stream.replace @unit %>
+6 -8
View File
@@ -1,17 +1,17 @@
<table class="main-area items" id="users"> <table class="main-area items-table" id="users">
<thead> <thead>
<tr> <tr>
<th><%= User.human_attribute_name(:email) %></th> <th><%= User.human_attribute_name(:email) %></th>
<th><%= User.human_attribute_name(:status) %></th> <th><%= User.human_attribute_name(:status) %></th>
<th><%= User.human_attribute_name(:confirmed_at) %></th> <th><%= User.human_attribute_name(:confirmed_at) %></th>
<th><%= User.human_attribute_name(:created_at) %>&nbsp;<sup>(UTC)</sup></th> <th><pre><%= User.human_attribute_name(:created_at) %> <sup>(UTC)</sup></pre></th>
<th><%= t :actions %></th> <th><%= t :actions %></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @users.each do |user| %> <% @users.each do |user| %>
<tr> <tr>
<td class="link"><%= link_to user, user_path(user) %></td> <td><%= link_to user, user_path(user), class: 'link' %></td>
<td> <td>
<% if user == current_user %> <% if user == current_user %>
<%= user.status %> <%= user.status %>
@@ -22,11 +22,9 @@
<% end %> <% end %>
<% end %> <% end %>
</td> </td>
<td class="svg"> <td><%= svg_tag 'checkbox-marked-outline' if user.confirmed_at.present? %></td>
<%= svg_tag 'pictograms/checkbox-marked-outline' if user.confirmed_at.present? %> <td><pre><%= l user.created_at, format: :without_tz %></pre></td>
</td> <td class="flex">
<td><%= l user.created_at, format: :without_tz %></td>
<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 %>
@@ -1,2 +1 @@
<%# For some reason flash messages are duplicated in bot flash and flash.now %>
<% flash.discard %> <% flash.discard %>
+1 -1
View File
@@ -1,5 +1,5 @@
<% 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('arrow-left-bold-outline', t(:back)),
request.referer.present? ? :back : root_path, class: 'tab' %> request.referer.present? ? :back : root_path, class: 'tab' %>
<% end %> <% end %>
+1 -2
View File
@@ -1,6 +1,5 @@
<% content_for :navigation, flush: true do %> <% content_for :navigation, flush: true do %>
<%= link_to svg_tag('pictograms/arrow-left-bold-outline', t(:back)), users_path, <%= link_to svg_tag('arrow-left-bold-outline', t(:back)), users_path, class: 'tab' %>
class: 'tab' %>
<% end %> <% end %>
<%= labeled_form_for @user, html: {class: 'main-area'} do |f| %> <%= labeled_form_for @user, html: {class: 'main-area'} do |f| %>
+1 -1
View File
@@ -8,7 +8,7 @@
<%= f.email_field :email, autofocus: true, autocomplete: "email" %> <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div> </div>
<div class="actions"> <div class="flex">
<%= f.submit "Resend unlock instructions" %> <%= f.submit "Resend unlock instructions" %>
</div> </div>
<% end %> <% end %>
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "active_support/continuous_integration"
CI = ActiveSupport::ContinuousIntegration
require_relative "../config/ci.rb"
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env ruby
exec "./bin/rails", "server", *ARGV
Regular → Executable
View File
Executable
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/ruby33
#
# This file was generated by RubyGems.
#
# The application 'nokogiri' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'rubygems'
Gem.use_gemdeps
version = ">= 0.a"
str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end
if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('nokogiri', 'nokogiri', version)
else
gem "nokogiri", version
load Gem.bin_path("nokogiri", "nokogiri", version)
end
+7 -5
View File
@@ -1,11 +1,10 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
require "fileutils" require "fileutils"
# path to your application root.
APP_ROOT = File.expand_path("..", __dir__) APP_ROOT = File.expand_path("..", __dir__)
def system!(*args) def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==") system(*args, exception: true)
end end
FileUtils.chdir APP_ROOT do FileUtils.chdir APP_ROOT do
@@ -14,7 +13,6 @@ FileUtils.chdir APP_ROOT do
# Add necessary setup steps to this file. # Add necessary setup steps to this file.
puts "== Installing dependencies ==" puts "== Installing dependencies =="
system! "gem install bundler --conservative"
system("bundle check") || system!("bundle install") system("bundle check") || system!("bundle install")
# puts "\n== Copying sample files ==" # puts "\n== Copying sample files =="
@@ -24,10 +22,14 @@ FileUtils.chdir APP_ROOT do
puts "\n== Preparing database ==" puts "\n== Preparing database =="
system! "bin/rails db:prepare" system! "bin/rails db:prepare"
system! "bin/rails db:reset" if ARGV.include?("--reset")
puts "\n== Removing old logs and tempfiles ==" puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear" system! "bin/rails log:clear tmp:clear"
puts "\n== Restarting application server ==" unless ARGV.include?("--skip-server")
system! "bin/rails restart" puts "\n== Starting development server =="
STDOUT.flush # flush the output before exec(2) so that it displays
exec "bin/dev"
end
end end
+13 -7
View File
@@ -18,14 +18,17 @@ require "rails/test_unit/railtie"
# you've limited to :test, :development, or :production. # you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups) Bundler.require(*Rails.groups)
require_relative '../lib/default_settings_strategy'
module FixinMe module FixinMe
class Application < Rails::Application class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version. # Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0 config.load_defaults 8.1
# Autoload lib/, required e.g. for core library extensions. # Please, add to the `ignore` list any other `lib` subdirectories that do
# https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#config-autoload-lib-ignore. # not contain `.rb` files, or that should not be reloaded or eager loaded.
config.autoload_lib(ignore: %w(assets tasks)) # Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w[assets tasks])
# Configuration for the application, engines, and railties goes here. # Configuration for the application, engines, and railties goes here.
# #
@@ -38,16 +41,19 @@ module FixinMe
config.action_dispatch.rescue_responses['ApplicationController::AccessForbidden'] = :forbidden config.action_dispatch.rescue_responses['ApplicationController::AccessForbidden'] = :forbidden
config.action_dispatch.rescue_responses['ApplicationController::ParameterInvalid'] = :unprocessable_entity config.action_dispatch.rescue_responses['ApplicationController::ParameterInvalid'] = :unprocessable_entity
# Set default migrations parameters.
config.active_record.migration_strategy = DefaultSettingsStrategy
# SETUP: Below settings need to be updated on a per-installation basis. # SETUP: Below settings need to be updated on a per-installation basis.
# #
# URL to use in sent e-mails. # Set host to be used by links generated in mailer templates.
config.action_mailer.default_url_options = {host: 'localhost', :protocol => 'https'} config.action_mailer.default_url_options = {host: 'localhost', protocol: 'https'}
# https://guides.rubyonrails.org/configuring.html#config-action-mailer-delivery-method # https://guides.rubyonrails.org/configuring.html#config-action-mailer-delivery-method
config.action_mailer.delivery_method = :sendmail config.action_mailer.delivery_method = :sendmail
# List of hosts this app is available at. # List of hosts this app is available at.
# https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization # https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization
config.hosts += ['localhost', 'example.com', IPAddr.new('1.2.3.4/32')] config.hosts |= ['localhost']
# Email address of admin account # Email address of admin account
config.admin = 'admin@localhost' config.admin = 'admin@localhost'
+20
View File
@@ -0,0 +1,20 @@
# Run using bin/ci
CI.run do
step "Setup", "bin/setup --skip-server"
step "Security: Importmap vulnerability audit", "bin/importmap audit"
step "Tests: Rails", "bin/rails test"
step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"
# Optional: Run system tests
# step "Tests: System", "bin/rails test:system"
# Optional: set a green GitHub commit status to unblock PR merge.
# Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
# if success?
# step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
# else
# failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
# end
end
+30 -12
View File
@@ -24,27 +24,45 @@
# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full overview on how database connection configuration can be specified. # for a full overview on how database connection configuration can be specified.
default: &default default: &default
adapter: mysql2 pool: <%= ENV.fetch('RAILS_MAX_THREADS', 3) %>
encoding: utf8mb4
collation: utf8mb4_0900_as_ci #mysql_default: &mysql_default
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> # <<: *default
username: fixinme # username: fixinme
password: Some-password1% # password: Some-password1%
socket: /run/mysqld/mysqld.sock # host: 127.0.0.1
# encoding: utf8mb4
# collation: utf8mb4_0900_as_ci
production: production:
<<: *default <<: *default
database: fixinme adapter: sqlite3
database: db/production.sqlite3
# Unless you're planning on developing the application, you can skip # Unless you're planning on developing the application, you can skip/remove
# configurations for development and test databases altogether. # configurations for development and test databases altogether.
#development: #development:
# <<: *default # <<: *mysql_default
# adapter: mysql2
# database: fixinme_dev # database: fixinme_dev
# Warning: The database defined as "test" will be erased and # Warning: The database(s) defined as "test" will be erased and
# re-generated from your development database when you run "rake". # re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production. # Do not set this db to the same as development or production.
#test: #test:
# <<: *default # <<: *mysql_default
# adapter: mysql2
# database: fixinme_test # database: fixinme_test
# Multiple test databases can be provided. When more than one test db is
# present, every test task (test, test:models, test:system, etc.) runs against
# all of them.
#test:
# mysql2:
# <<: *mysql_default
# adapter: mysql2
# database: fixinme_test
# sqlite3:
# <<: *default
# adapter: sqlite3
# database: db/test.sqlite3
+21 -25
View File
@@ -3,10 +3,8 @@ require "active_support/core_ext/integer/time"
Rails.application.configure do Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb. # Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded any time # Make code changes take effect immediately without server restart.
# it changes. This slows down response time but is perfect for development config.enable_reloading = true
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Do not eager load code on boot. # Do not eager load code on boot.
config.eager_load = false config.eager_load = false
@@ -14,54 +12,52 @@ Rails.application.configure do
# Show full error reports. # Show full error reports.
config.consider_all_requests_local = true config.consider_all_requests_local = true
# Enable server timing # Enable server timing.
config.server_timing = true config.server_timing = true
# Enable/disable caching. By default caching is disabled. # Enable/disable Action Controller caching. By default Action Controller caching is disabled.
# Run rails dev:cache to toggle caching. # Run rails dev:cache to toggle Action Controller caching.
if Rails.root.join("tmp/caching-dev.txt").exist? if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true config.action_controller.enable_fragment_cache_logging = true
config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
config.cache_store = :memory_store
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{2.days.to_i}"
}
else else
config.action_controller.perform_caching = false config.action_controller.perform_caching = false
config.cache_store = :null_store
end end
# Change to :null_store to avoid any caching.
config.cache_store = :memory_store
# Don't care if the mailer can't send. # Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = false
# Make template changes take effect immediately.
config.action_mailer.perform_caching = false config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger. # Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raise an error on page load if there are pending migrations. # Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs. # Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true config.active_record.verbose_query_logs = true
# Suppress logger output for asset requests. # Append comments with runtime information tags to SQL queries in logs.
config.assets.quiet = true config.active_record.query_log_tags_enabled = true
# Highlight code that enqueued background job in logs.
config.active_job.verbose_enqueue_logs = true
# Highlight code that triggered redirect in logs.
config.action_dispatch.verbose_redirect_logs = true
# Raises error for missing translations. # Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true # config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names. # Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true config.action_view.annotate_rendered_view_with_filenames = true
# Uncomment if you wish to allow Action Cable access from any origin. # Raise error when a before_action's only/except options reference missing actions.
# config.action_cable.disable_request_forgery_protection = true config.action_controller.raise_on_missing_callback_actions = true
end end
+50 -45
View File
@@ -4,78 +4,83 @@ Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb. # Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests. # Code is not reloaded between requests.
config.cache_classes = true config.enable_reloading = false
# Eager load code on boot. This eager loads most of Rails and # Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true config.eager_load = true
# Full error reports are disabled and caching is turned on. # Full error reports are disabled.
config.consider_all_requests_local = false config.consider_all_requests_local = false
# Turn on fragment caching in view templates.
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # Cache assets for far-future expiry since they are all digest stamped.
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files). config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }
# config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Enable serving of images, stylesheets, and JavaScripts from an asset server. # Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com" # config.asset_host = "http://assets.example.com"
# Specifies the header that your server uses for sending files. # Assume all access to the app is happening through a SSL-terminating reverse proxy.
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache config.assume_ssl = true
# config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true config.force_ssl = true
# Include generic and useful information about system operation, but avoid logging too much # Skip http-to-https redirect for the default health check endpoint.
# information to avoid inadvertent exposure of personally identifiable information (PII). # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
config.log_level = :info
# Prepend all log lines with the following tags. # Log to STDOUT with the current request id as a default log tag.
config.log_tags = [ :request_id ] config.log_tags = [ :request_id ]
config.logger = ActiveSupport::TaggedLogging.logger(STDOUT)
# Use a different cache store in production. # Change to "debug" to log everything (including potentially personally-identifiable information!).
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
# Prevent health checks from clogging up the logs.
config.silence_healthcheck_path = "/up"
# Don't log any deprecations.
config.active_support.report_deprecations = false
# Replace the default in-process memory cache store with a durable alternative.
# config.cache_store = :mem_cache_store # config.cache_store = :mem_cache_store
config.action_mailer.perform_caching = false # Replace the default in-process and non-durable queuing backend for Active Job.
# config.active_job.queue_adapter = :resque
# Ignore bad email addresses and do not raise email delivery errors. # Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false # config.action_mailer.raise_delivery_errors = false
# Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
# config.action_mailer.smtp_settings = {
# user_name: Rails.application.credentials.dig(:smtp, :user_name),
# password: Rails.application.credentials.dig(:smtp, :password),
# address: "smtp.example.com",
# port: 587,
# authentication: :plain
# }
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found). # the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true config.i18n.fallbacks = true
# Don't log any deprecations.
config.active_support.report_deprecations = false
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require "syslog/logger"
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations. # Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false config.active_record.dump_schema_after_migration = false
# Only use :id for inspections in production.
config.active_record.attributes_for_inspect = [ :id ]
# Enable DNS rebinding protection and other `Host` header attacks.
# config.hosts = [
# "example.com", # Allow requests from example.com
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
# ]
#
# Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
# Protect e-mail addresses from being logged only in production.
config.filter_parameters += [:email]
end end
+19 -26
View File
@@ -1,5 +1,3 @@
require "active_support/core_ext/integer/time"
# The test environment is used exclusively to run your application's # The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that # test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped # your test database is "scratch space" for the test suite and is wiped
@@ -8,54 +6,49 @@ require "active_support/core_ext/integer/time"
Rails.application.configure do Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb. # Settings specified here will take precedence over those in config/application.rb.
# Turn false under Spring and add config.action_view.cache_template_loading = true. # While tests run files are not watched, reloading is not necessary.
config.cache_classes = true config.enable_reloading = false
# Eager loading loads your whole application. When running a single test locally, # Eager loading loads your entire application. When running a single test locally,
# this probably isn't necessary. It's a good idea to do in a continuous integration # this is usually not necessary, and can slow down your test suite. However, it's
# system, or in some way before deploying your code. # recommended that you enable it in continuous integration systems to ensure eager
# loading is working properly before deploying your code.
config.eager_load = ENV["CI"].present? config.eager_load = ENV["CI"].present?
# Configure public file server for tests with Cache-Control for performance. # Configure public file server for tests with cache-control for performance.
config.public_file_server.enabled = true config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{1.hour.to_i}"
}
# Hide full error reports. # Behave as in `production`.
config.consider_all_requests_local = false config.consider_all_requests_local = false
# Render exception templates instead of raising exceptions.
config.action_dispatch.show_exceptions = :all
# Disable caching.
config.action_controller.perform_caching = false
config.cache_store = :null_store config.cache_store = :null_store
# Disable request forgery protection in test environment. # Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false config.action_controller.allow_forgery_protection = false
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world. # Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the # The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array. # ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = {host: '127.0.0.1', :protocol => 'http'}
# Set host to be used by links generated in mailer templates.
config.action_mailer.default_url_options = {host: Capybara.server_host,
protocol: 'http'}
# Print deprecation notices to the stderr. # Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr config.active_support.deprecation = :stderr
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raises error for missing translations. # Raises error for missing translations.
config.i18n.raise_on_missing_translations = true config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names. # Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true # config.action_view.annotate_rendered_view_with_filenames = true
# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
# Allow Capybara application server IP
config.hosts |= [IPAddr.new(Capybara.server_host)]
config.log_level = :info config.log_level = :info
end end
-5
View File
@@ -5,8 +5,3 @@ Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path. # Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path # Rails.application.config.assets.paths << Emoji.images_path
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
Rails.application.config.assets.precompile += %w( '*.svg' )
@@ -16,9 +16,13 @@
# # policy.report_uri "/csp-violation-report-endpoint" # # policy.report_uri "/csp-violation-report-endpoint"
# end # end
# #
# # Generate session nonces for permitted importmap and inline scripts # # Generate session nonces for permitted importmap, inline scripts, and inline styles.
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src) # config.content_security_policy_nonce_directives = %w(script-src style-src)
#
# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
# # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
# # config.content_security_policy_nonce_auto = true
# #
# # Report violations without enforcing the policy. # # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true # # config.content_security_policy_report_only = true
+6 -6
View File
@@ -1,17 +1,17 @@
require 'core_ext/array_delete_bang' require 'core_ext/array_delete_bang'
require 'core_ext/big_decimal_scientific_notation' require 'core_ext/float'
require 'core_ext/range'
ActiveSupport.on_load :action_dispatch_system_test_case do ActiveSupport.on_load :action_dispatch_system_test_case do
prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId prepend CoreExt::ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelperUniqueId
end # Allow denormalized string comparison,
# https://github.com/orgs/teamcapybara/discussions/2841
ActiveSupport.on_load :action_view do XPath::DSL.prepend CoreExt::XPath::DSL::NormalizeSpaceDefault
ActionView::RecordIdentifier.prepend CoreExt::ActionView::RecordIdentifierWithSuffix
end end
ActiveSupport.on_load :active_record do ActiveSupport.on_load :active_record do
ActiveModel::Validations::NumericalityValidator ActiveModel::Validations::NumericalityValidator
.prepend CoreExt::ActiveModel::Validations::NumericalityValidatesPrecisionAndScale .prepend CoreExt::ActiveModel::Validations::NumericalityValidatesPrecision
# Temporary patch for https://github.com/rails/rails/pull/54658 # Temporary patch for https://github.com/rails/rails/pull/54658
Arel::TreeManager::StatementMethods Arel::TreeManager::StatementMethods

Some files were not shown because too many files have changed in this diff Show More