inline_forms 8.1.64 → 8.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -1
  3. data/app/assets/javascripts/inline_forms/inline_forms.js +23 -7
  4. data/app/assets/stylesheets/inline_forms/foundation_and_overrides.scss +8 -1
  5. data/app/assets/stylesheets/inline_forms/inline_forms.scss +10 -0
  6. data/app/helpers/inline_forms_helper.rb +15 -0
  7. data/app/views/devise/inline_forms/_content_for_devise_login.html.erb +3 -2
  8. data/app/views/devise/passwords/_top-bar-and-flash.html.erb +1 -1
  9. data/app/views/devise/sessions/_top-bar.html.erb +1 -1
  10. data/app/views/devise/sessions/new.html.erb +1 -1
  11. data/app/views/inline_forms/_generic_search.html.erb +3 -3
  12. data/app/views/inline_forms/_versions_file_entry.html.erb +1 -1
  13. data/app/views/inline_forms/_versions_list.html.erb +1 -1
  14. data/app/views/inline_forms/file_trash/index.html.erb +4 -4
  15. data/app/views/inline_forms/file_trash_panel.html.erb +3 -3
  16. data/app/views/layouts/application.html.erb +1 -1
  17. data/app/views/layouts/devise.html.erb +1 -1
  18. data/app/views/layouts/inline_forms.html.erb +9 -1
  19. data/lib/inline_forms/dropdown_order.rb +20 -7
  20. data/lib/inline_forms/form_elements/pdf_link_helper.rb +8 -1
  21. data/lib/inline_forms/searchable.rb +6 -4
  22. data/lib/inline_forms/version.rb +1 -1
  23. data/lib/locales/inline_forms.en.yml +15 -4
  24. data/lib/locales/inline_forms.nl.yml +15 -4
  25. data/test/integration/dropdown_test.rb +40 -0
  26. data/test/integration/legacy_elements_test.rb +17 -0
  27. data/test/integration/locale_test.rb +57 -2
  28. data/test/integration/model_top_bar_test.rb +18 -0
  29. data/test/integration/turbo_menu_test.rb +46 -0
  30. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d65f5d0ad495091211688c0db04b4466e240ea578b1349523e97a1ce66d3cd00
4
- data.tar.gz: 9aca4b16c7481547715c240283ff8bac4de6d1d19caa57e3bc239aa51ed2899c
3
+ metadata.gz: 7d798e9e7a6d981b422009a82073f77675148cfcd17263b501067d7d6e0a3b57
4
+ data.tar.gz: 76876893fbad74dd364b02c21f927bc414cb86b2b6e7a89b1caa5a3a941bab4d
5
5
  SHA512:
6
- metadata.gz: 05afb1bc303bfdda7506735a166f1f69f0ee5f8a79efa2279a4d928e308c69047037bfd00383a260a3e547f5540bdd7f36d8aff476da0c54c9c7b92df21a39b2
7
- data.tar.gz: c059604b46b459595cc34fb574704a1e0203aab6eb40083825df0763c0e9ae48489765515529efa30561f0b2bed44ec39944566750b7e6fc14b7da2c4d209033
6
+ metadata.gz: d1c4624d5315833d86d2578112d2f494b8f7cd32219698de060afbc7570a638fb98c5bf07fd376de87854b264c85521033da2c88920093c3552b5925c5062c8b
7
+ data.tar.gz: dab2278a87d37e9005baafc360503a0a767cbd95ce1e20ad0484463098d65763547daac3f25c6e16ca1707ca8ee92ef7e9f922ffdfce996c74670af849f028bc
data/CHANGELOG.md CHANGED
@@ -4,6 +4,68 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Known issues
8
+
9
+ - Known issues, parked (local note `stuff/2026-10-31-dropdown-rule-3-parked.md`) with dropdown order rule 3: `includes`, `group` or `select` in `inline_forms_list` make `dropdown` and `dropdown_with_other` raise while `/index` works, so 8.1.65's fix for a list order on a joined table holds only for `joins`, `left_joins` and `eager_load`; a has_many join gives duplicate options. Present since 8.1.60. No host's list scope has these today. Parked; not changed.
10
+
11
+ ## [8.1.66] - 2026-11-06
12
+
13
+ Requests from Papiamentu's move to the 8.1.65 admin (gem layout, engine model top bar, gem stylesheet); each was worked around in the apps.
14
+
15
+ ### Fixed
16
+
17
+ - **Top-bar submenus no longer flash open after a Turbo Drive visit.** Turbo replaced `<body>` and re-ran the inline_forms bundle, which loaded a fresh jQuery; until it finished, the new body's "more..." and user submenus showed as plain lists. StProject, Scouting and Papiamentu each copied the admin layout to fix it. Now `layouts/inline_forms` starts as `<html class="no-js">`; `inline_forms.js` re-initialises Foundation on `turbo:load`, behind a once-guard (Turbo still re-runs the bundle on each visit, so a visit from a layout without it works; the guard stops the listeners piling up and a second `foundation()` from the fresh jQuery); and `inline_forms.scss` hides a submenu until Foundation has marked it `is-dropdown-submenu`.
18
+ - **No more 404 for `/vendor/assets/stylesheets/tippy.css`.** `foundation_and_overrides.scss` imported `tippy.css` with its extension, which Dart Sass keeps as a plain CSS `@import`; the browser resolved it next to the compiled stylesheet. The vendored file is now inlined with `meta.load-css` (no new `@import` deprecation), in admin and devise CSS. Apps can delete their `public/vendor/assets/stylesheets/tippy.css`.
19
+ - **Version and file timestamps show the year.** The versions list, the file entries in it and the file trash used `l(time, format: :short)`, and rails-i18n's `:short` has no year. They now use the gem's `time.formats.inline_forms` (`"%d %b %Y %H:%M"` in en and nl; a host can override it). The versions list and file entries render a `<time>` with the ISO 8601 time as `datetime` and `title` (new helper `inline_forms_time`).
20
+ - **The generic search box is translated.** `_generic_search` had "zoek op naam...", "zoek" and "reset" hard-coded; now `inline_forms.search.placeholder` / `.submit` / `.reset` (en: "search...", "search", "reset"; nl: "zoeken...", "zoek", "wissen").
21
+ - **No placeholder titles.** The gem's en and nl locales defined `general.application_title: Application Title`, `devise.title_for_devise: Title for Devise` and `devise.welcome: Welcome for Devise`, shown in any locale the host did not override (the installer only writes en). The keys are gone; the application and devise layouts and the devise top bars fall back to the host's `application_name` translation, else the Rails module name (new helper `inline_forms_application_title`), and the sign-in page to `inline_forms.devise.welcome_to` ("Welcome to %{name}!" / "Welkom bij %{name}!"). A host's own translations still win.
22
+ - **`inline_forms_search_on` no longer touches the database when the model loads.** It quoted the table and column names in the class body, so loading the model needed a connection; the clause is now built when the scope runs.
23
+
24
+ ### Changed
25
+
26
+ - **Installer: the Dart Sass self-import guard no longer patches dartsass-rails.** The generated `config/initializers/inline_forms_dartsass_builds.rb` prepended the gem's stylesheet dir to `Dartsass::Runner.dartsass_load_paths`, an internal (a silent no-op if renamed; a boot error if `dartsass/runner` moved). The two `inline_forms_install/*_main.scss` entries now `@use "inline_forms/inline_forms.scss"` and `"inline_forms/devise.scss"` with the extension, which Dart Sass never resolves to a previous `app/assets/builds/*.css`, and which fails loudly if the gem renames the file. Existing apps keep their initializer until they change it.
27
+
28
+ ### Not done
29
+
30
+ - **Autocomplete in the generic search** (a `rails-jquery-autocomplete` field when the controller declares `autocomplete :<model>, :name`): that gem needs jQuery UI, which inline_forms dropped in 8.1.26, and it is not an inline_forms dependency. An app that wants it keeps its own `_generic_search` override.
31
+
32
+ ### Tests
33
+
34
+ - `turbo_menu_test` (new): the layout's `no-js`, the once-guarded `turbo:load` re-init, the submenu rule, tippy inlined.
35
+ - `locale_test`: timestamps with the year and an ISO `title`, the search box in en and nl, no placeholder titles (devise and application layouts, the devise top bar).
36
+ - `model_top_bar_test`: `inline_forms_search_on` declared on a model whose connection raises.
37
+ - Example app: `example_app_stylesheet_test` (new) checks the compiled CSS has no `@import`, inlines tippy and hides uninitialised submenus; `example_app_dartsass_build_test` checks the `.scss` extensions instead of the patched load path. `example_app_user_locale_test` and `example_app_layout_title_test` expect `<html lang="…" class="no-js">`.
38
+
39
+ ### Lockstep
40
+
41
+ - validation_hints 8.1.66, inline_forms_installer 8.1.66, inline_forms_schema_edit 8.1.66.
42
+
43
+ ## [8.1.65] - 2026-10-31
44
+
45
+ Fixes from a review of 8.1.60 to 8.1.64.
46
+
47
+ ### Fixed
48
+
49
+ - **Dropdowns honour a `reorder` in `inline_forms_list`.** Since 8.1.60 rule 3 added the scope's order to a `default_scope` order, so a scope that reorders lost: Scouting's lookups with `inline_forms_list_by_name` would have sorted by name and lost their pinned entries. Rule 3 now uses the scope's order as it stands: it follows a `default_scope` order, unless the scope reorders.
50
+ - **A list order on a joined table no longer breaks the dropdown.** Since 8.1.60 rule 3 dropped the scope's joins and kept its ORDER BY, which was invalid SQL. Association joins now come along as LEFT joins, so the order works and no record is hidden. A string join stays as written, so it can still hide records.
51
+ - **`pdf_link` needs no model method.** `pdf_link_edit` called the attribute, so a new form with a `pdf_link` row raised `NoMethodError` unless the model had a stub. The example app's `def report` is gone.
52
+ - **`pdf_link` works under a sub-path.** Both links now start with the app's `request.script_name`.
53
+
54
+ ### Tests
55
+
56
+ - `dropdown_test`: `default_scope` with a list scope, a `reorder`, and a list order on a joined table (`dropdown` and `check_list`).
57
+ - `legacy_elements_test`: `pdf_link` in a new form without a model method, and under a sub-path.
58
+ - Example app: `example_app_seed_rollback_test` runs the seed migration's `down`.
59
+
60
+ ### Documentation
61
+
62
+ - `CLAUDE.md`: inline_forms is released with `rake release:all`.
63
+ - The 8.1.59 changelog date is the release date, 2026-10-25.
64
+
65
+ ### Lockstep
66
+
67
+ - validation_hints 8.1.65, inline_forms_installer 8.1.65, inline_forms_schema_edit 8.1.65.
68
+
7
69
  ## [8.1.64] - 2026-10-31
8
70
 
9
71
  ### Changed
@@ -82,7 +144,7 @@ Fixes from a review of 8.1.58 and 8.1.59.
82
144
 
83
145
  - validation_hints 8.1.60, inline_forms_installer 8.1.60, inline_forms_schema_edit 8.1.60.
84
146
 
85
- ## [8.1.59] - 2026-10-22
147
+ ## [8.1.59] - 2026-10-25
86
148
 
87
149
  ### Fixed
88
150
 
@@ -14,14 +14,30 @@
14
14
  // dropdown_with_other combobox is an <input list> + <datalist>, and
15
15
  // slider_with_values is a native <input type="range">. jQuery itself stays
16
16
  // only because Foundation 6's JS requires it.
17
- $(function(){
18
- $(document).foundation();
19
- initInlineFormsWidgets(document);
20
- });
17
+ // Once per page load. Turbo Drive re-runs this bundle on every visit (it sits
18
+ // in <body>), and each run brings a fresh jQuery: without the guard each run
19
+ // added another turbo:load listener, and a ready-handler `foundation()` from
20
+ // the fresh copy (its own plugin data) would initialise the menus twice.
21
+ if (!window.inlineFormsBooted) {
22
+ window.inlineFormsBooted = true;
21
23
 
22
- document.addEventListener("turbo:load", function() {
23
- initInlineFormsWidgets(document);
24
- });
24
+ $(function(){
25
+ $(document).foundation();
26
+ initInlineFormsWidgets(document);
27
+ });
28
+
29
+ // Turbo Drive replaces <body> on a visit: the new top bar's DropdownMenu
30
+ // has to be initialised again, or its submenus show as plain lists.
31
+ // Foundation's reflow skips elements that already carry a plugin, so the
32
+ // call on the first page (turbo:load fires there too) is harmless. jQuery
33
+ // is looked up at call time.
34
+ document.addEventListener("turbo:load", function() {
35
+ if (window.jQuery && window.jQuery.fn.foundation) {
36
+ window.jQuery(document).foundation();
37
+ }
38
+ initInlineFormsWidgets(document);
39
+ });
40
+ }
25
41
 
26
42
  // Widget init: one path for first paint, turbo:load and turbo:frame-load.
27
43
  // Form element helpers emit class hooks only — no inline <script> tags.
@@ -1,5 +1,7 @@
1
1
  @charset 'utf-8';
2
2
 
3
+ @use 'sass:meta';
4
+
3
5
  @import 'settings/settings';
4
6
 
5
7
  // Match F5 visual proportions: F5 `$row-width` was 62.5rem (1000px); F6
@@ -58,7 +60,12 @@ $body-font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Roboto, Arial, sans
58
60
  // Stays @import: see the module-system note in inline_forms.scss.
59
61
  @import 'theme';
60
62
 
61
- @import '../../../vendor/assets/stylesheets/tippy.css';
63
+ // Tippy's vendored CSS, inlined. It used to be `@import '…/tippy.css'`,
64
+ // which Dart Sass passes through as a plain CSS @import: the browser then
65
+ // fetched /vendor/assets/stylesheets/tippy.css (resolved next to the compiled
66
+ // stylesheet), a 404 on every page. load-css (not an extension-less @import,
67
+ // deprecated for Dart Sass 3) emits it here, in the same place.
68
+ @include meta.load-css('../../../vendor/assets/stylesheets/tippy');
62
69
 
63
70
  .validation-hints-source[hidden] {
64
71
  display: none !important;
@@ -113,6 +113,16 @@
113
113
  }
114
114
  }
115
115
 
116
+ // A top-bar submenu shows as a plain list until Foundation's DropdownMenu
117
+ // marks it `is-dropdown-submenu` (whose own rules then hide it). On the first
118
+ // page `html.no-js` covers that gap (layouts/inline_forms.html.erb), but
119
+ // Foundation removes the class and Turbo Drive never restores it, so after a
120
+ // visit the new body's submenus flashed open until inline_forms.js
121
+ // re-initialised them on turbo:load.
122
+ .dropdown.menu > li > ul.menu:not(.is-dropdown-submenu) {
123
+ display: none;
124
+ }
125
+
116
126
  // Match: model top bar is the wider gold band (height 90px, padded so its
117
127
  // usable area is in the lower 45px). Override Foundation 6's default
118
128
  // `.top-bar { padding: 0.5rem }` so our 45/90px math holds.
@@ -394,6 +394,21 @@ module InlineFormsHelper
394
394
  values
395
395
  end
396
396
 
397
+ # A version or file timestamp: the date with its year
398
+ # (time.formats.inline_forms; rails-i18n's :short has none, so edits years
399
+ # apart read the same), the exact ISO 8601 time on hover.
400
+ def inline_forms_time(time)
401
+ tag.time(l(time, format: :inline_forms), datetime: time.iso8601, title: time.iso8601)
402
+ end
403
+
404
+ # The application's name for the gem's titles and the Devise pages: the
405
+ # host's `application_name` translation, else its Rails module name. The
406
+ # gem used to ship "Application Title" / "Title for Devise" placeholders,
407
+ # shown in every locale the host did not override (nl, in all of them).
408
+ def inline_forms_application_title
409
+ t("application_name", default: Rails.application.class.module_parent_name.titleize)
410
+ end
411
+
397
412
  def version_modified_by(id)
398
413
  return "Unknown" if id.blank?
399
414
 
@@ -6,7 +6,8 @@
6
6
  and the contents will be displayed here.
7
7
  </p>
8
8
  <p>
9
- Change application title, title_for_devise, welcome_for_devise in<br />
10
- <em>config/locales/inline_forms.en.yml</em><br />
9
+ The titles come from the <em>application_name</em> translation; override
10
+ inline_forms.general.application_title, inline_forms.devise.title_for_devise
11
+ or inline_forms.devise.welcome in <em>config/locales</em> to change one.<br />
11
12
  </p>
12
13
 
@@ -3,7 +3,7 @@
3
3
  <div class="top-bar-left">
4
4
  <ul class="menu">
5
5
  <li class="menu-text">
6
- <h1 style="margin: 0; font-size: inherit;"><a href='/'><%= t('inline_forms.devise.title_for_devise') %> <%= InlineForms::VERSION %></a></h1>
6
+ <h1 style="margin: 0; font-size: inherit;"><a href='/'><%= t('inline_forms.devise.title_for_devise', default: inline_forms_application_title) %> <%= InlineForms::VERSION %></a></h1>
7
7
  </li>
8
8
  </ul>
9
9
  </div>
@@ -3,7 +3,7 @@
3
3
  <div class="top-bar-left">
4
4
  <ul class="menu">
5
5
  <li class="menu-text">
6
- <h1 style="margin: 0; font-size: inherit;"><a href='/'><%= t('inline_forms.devise.title_for_devise') %> <%= InlineForms::VERSION %></a></h1>
6
+ <h1 style="margin: 0; font-size: inherit;"><a href='/'><%= t('inline_forms.devise.title_for_devise', default: inline_forms_application_title) %> <%= InlineForms::VERSION %></a></h1>
7
7
  </li>
8
8
  </ul>
9
9
  </div>
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div class="row" id='inline_forms_devise_header_row'>
6
6
  <div class="small-11 centered column">
7
- <h1><%= t('inline_forms.devise.welcome') %></h1>
7
+ <h1><%= t('inline_forms.devise.welcome', default: t('inline_forms.devise.welcome_to', name: inline_forms_application_title)) %></h1>
8
8
  </div>
9
9
  </div>
10
10
 
@@ -1,13 +1,13 @@
1
1
  <%= form_tag request.path, method: :get do %>
2
2
  <div class="row collapse">
3
3
  <div class="large-1 small-1 columns">
4
- <%= link_to "<i class='fi-x'></i>".html_safe, request.path, title: "reset", class: "button expanded inline_forms_model_top_bar_buttons" %>
4
+ <%= link_to tag.i(class: "fi-x"), request.path, title: t("inline_forms.search.reset"), class: "button expanded inline_forms_model_top_bar_buttons" %>
5
5
  </div>
6
6
  <div class="large-6 small-6 columns">
7
- <%= text_field_tag :search, params[:search], placeholder: "zoek op naam...", id: "input_search" %>
7
+ <%= text_field_tag :search, params[:search], placeholder: t("inline_forms.search.placeholder"), id: "input_search" %>
8
8
  </div>
9
9
  <div class="large-3 small-3 columns">
10
- <%= submit_tag "zoek", class: "button expanded inline_forms_model_top_bar_buttons" %>
10
+ <%= submit_tag t("inline_forms.search.submit"), class: "button expanded inline_forms_model_top_bar_buttons" %>
11
11
  </div>
12
12
  <div class="large-2 small-2 columns">
13
13
  &nbsp;
@@ -23,7 +23,7 @@
23
23
  <%= t("inline_forms.files.event.#{entry[:file_event]}") %>
24
24
  </div>
25
25
  <div class="small-2 column">
26
- <%= l(entry[:at], format: :short) %>
26
+ <%= inline_forms_time(entry[:at]) %>
27
27
  </div>
28
28
  <div class="small-2 column">
29
29
  <%= inline_forms_file_user_name(entry[:by]) %>
@@ -95,7 +95,7 @@
95
95
  <%= t("inline_forms.view.version_events.#{version.event}", default: version.event) -%><% if entry[:kind] == :rich_text %> <em>(<%= t("inline_forms.view.versions_rich_text") %>)</em><% end %>
96
96
  </div>
97
97
  <div class="small-2 column">
98
- <%= l(version.created_at, format: :short) -%>
98
+ <%= inline_forms_time(version.created_at) -%>
99
99
  </div>
100
100
  <div class="small-2 column">
101
101
  <%= version_modified_by version.whodunnit -%>
@@ -14,7 +14,7 @@
14
14
  <% elsif @last_sweep.error.present? %>
15
15
  <%= t("inline_forms.files.global.sweep_failed", error: @last_sweep.error) %>
16
16
  <% else %>
17
- <%= t("inline_forms.files.global.sweep_last", at: l(@last_sweep.ran_at, format: :short), count: @last_sweep.purged_count) %>
17
+ <%= t("inline_forms.files.global.sweep_last", at: l(@last_sweep.ran_at, format: :inline_forms), count: @last_sweep.purged_count) %>
18
18
  <% end %>
19
19
  </p>
20
20
 
@@ -78,10 +78,10 @@
78
78
  <span class="inline_forms-file-name"><%= inline_forms_file_entry_name(entry) %></span>
79
79
  <span class="inline_forms-file-meta"><%= inline_forms_file_size(entry.byte_size) %> &middot; <%= t("inline_forms.files.reason.#{entry.reason}") %></span>
80
80
  </td>
81
- <td><%= t("inline_forms.files.by_on", user: inline_forms_file_user_name(entry.trashed_by_id), at: l(entry.trashed_at, format: :short)) %></td>
81
+ <td><%= t("inline_forms.files.by_on", user: inline_forms_file_user_name(entry.trashed_by_id), at: l(entry.trashed_at, format: :inline_forms)) %></td>
82
82
  <td>
83
- <% if entry.state == :restored %><%= t("inline_forms.files.state_restored", user: inline_forms_file_user_name(entry.restored_by_id), at: l(entry.restored_at, format: :short)) %>
84
- <% elsif entry.state == :purged %><%= t("inline_forms.files.state_purged", user: inline_forms_file_user_name(entry.purged_by_id), at: l(entry.purged_at, format: :short)) %>
83
+ <% if entry.state == :restored %><%= t("inline_forms.files.state_restored", user: inline_forms_file_user_name(entry.restored_by_id), at: l(entry.restored_at, format: :inline_forms)) %>
84
+ <% elsif entry.state == :purged %><%= t("inline_forms.files.state_purged", user: inline_forms_file_user_name(entry.purged_by_id), at: l(entry.purged_at, format: :inline_forms)) %>
85
85
  <% elsif entry.state == :expired %><%= t("inline_forms.files.state_expired") %>
86
86
  <% else %>
87
87
  <% if entry.purge_after %><%= t("inline_forms.files.expires_in", count: ((entry.purge_after - Time.current) / 1.day).ceil, date: l(entry.purge_after.to_date)) %><% else %><%= t("inline_forms.files.global.states.trashed") %><% end %>
@@ -41,13 +41,13 @@
41
41
  <span class="inline_forms-file-meta">
42
42
  <%= inline_forms_file_size(entry.byte_size) %> &middot;
43
43
  <%= t("inline_forms.files.reason.#{entry.reason}") %>
44
- <%= t("inline_forms.files.by_on", user: inline_forms_file_user_name(entry.trashed_by_id), at: l(entry.trashed_at, format: :short)) %>
44
+ <%= t("inline_forms.files.by_on", user: inline_forms_file_user_name(entry.trashed_by_id), at: l(entry.trashed_at, format: :inline_forms)) %>
45
45
  </span>
46
46
  <span class="inline_forms-file-meta">
47
47
  <% if entry.state == :restored %>
48
- <%= t("inline_forms.files.state_restored", user: inline_forms_file_user_name(entry.restored_by_id), at: l(entry.restored_at, format: :short)) %>
48
+ <%= t("inline_forms.files.state_restored", user: inline_forms_file_user_name(entry.restored_by_id), at: l(entry.restored_at, format: :inline_forms)) %>
49
49
  <% elsif entry.state == :purged %>
50
- <%= t("inline_forms.files.state_purged", user: inline_forms_file_user_name(entry.purged_by_id), at: l(entry.purged_at, format: :short)) %>
50
+ <%= t("inline_forms.files.state_purged", user: inline_forms_file_user_name(entry.purged_by_id), at: l(entry.purged_at, format: :inline_forms)) %>
51
51
  <% elsif entry.state == :expired %>
52
52
  <%= t("inline_forms.files.state_expired") %>
53
53
  <% else %>
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>
7
- <%= t('inline_forms.general.application_title') %> v<%= InlineForms::VERSION %>
7
+ <%= t('inline_forms.general.application_title', default: inline_forms_application_title) %> v<%= InlineForms::VERSION %>
8
8
  </title>
9
9
  <%= stylesheet_link_tag "application" %>
10
10
  <%= stylesheet_link_tag "inline_forms_fonts" %>
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>
7
- <%= t('inline_forms.general.application_title') %> v<%= InlineForms::VERSION %>
7
+ <%= t('inline_forms.general.application_title', default: inline_forms_application_title) %> v<%= InlineForms::VERSION %>
8
8
  </title>
9
9
  <%= stylesheet_link_tag "inline_forms_fonts" %>
10
10
  <%= stylesheet_link_tag "inline_forms/devise" %>
@@ -1,5 +1,10 @@
1
1
  <!DOCTYPE html>
2
- <html lang="<%= I18n.locale %>">
2
+ <%# class="no-js": Foundation's CSS hides the top-bar submenus while it is set %>
3
+ <%# and its JS removes it on the first init, so the submenus do not show as %>
4
+ <%# plain lists before then. Turbo Drive never puts it back, so later visits %>
5
+ <%# rely on the `ul.menu:not(.is-dropdown-submenu)` rule in inline_forms.scss %>
6
+ <%# and on the turbo:load handler in inline_forms.js. %>
7
+ <html lang="<%= I18n.locale %>" class="no-js">
3
8
  <head>
4
9
  <meta charset="utf-8" />
5
10
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -37,6 +42,9 @@
37
42
  </div>
38
43
  <%# Tippy.js (v6) needs Popper.js as a global before its UMD factory runs %>
39
44
  <%# (`tippy-bundle.umd.min` only bundles default themes, not Popper itself). %>
45
+ <%# Turbo Drive re-runs these on every visit (no data-turbo-eval="false": %>
46
+ <%# a visit from a page whose layout lacks them must still load them). %>
47
+ <%# inline_forms.js boots once and re-initialises Foundation on turbo:load. %>
40
48
  <%= javascript_include_tag 'popper.min' %>
41
49
  <%= javascript_include_tag 'tippy-bundle.umd.min' %>
42
50
  <%= javascript_include_tag 'inline_forms/inline_forms' %>
@@ -8,12 +8,14 @@ module InlineForms
8
8
  # 2. the table has a `dropdown_order` column (ascending), so a lookup can
9
9
  # put '---' / 'onbekend' first and process phases in process order;
10
10
  # 3. the ORDER BY of the model's `inline_forms_list` scope (its list order;
11
- # only the order: a where/joins in that scope does not hide records here);
11
+ # only the order: a where in that scope does not hide records here, and
12
+ # its joins come along as LEFT joins, for an order on a joined table);
12
13
  # 4. database order (a default_scope order included).
13
14
  #
14
15
  # 1 and 2 replace any default_scope order, put NULLs last (a record nobody
15
- # numbered yet goes to the end) and break ties on the primary key. 3 adds to
16
- # a default_scope order, as merging the scope did.
16
+ # numbered yet goes to the end) and break ties on the primary key. 3 keeps a
17
+ # default_scope order after it, unless the scope reorders: the scope's own
18
+ # order_values already say which.
17
19
  #
18
20
  # The macro is on every Active Record model (engine initializer), so hosts
19
21
  # need no ApplicationRecord change.
@@ -53,15 +55,26 @@ module InlineForms
53
55
  keys = [ table[column].public_send(direction).nulls_last ]
54
56
  keys << table[klass.primary_key].asc if klass.primary_key && klass.primary_key != column
55
57
  relation.reorder(*keys)
56
- elsif (list_order = list_order_values(klass)).any?
57
- relation.order(*list_order)
58
+ elsif (list = list_scope(klass))&.order_values&.any?
59
+ with_list_joins(relation, list).reorder(*list.order_values)
58
60
  else
59
61
  relation
60
62
  end
61
63
  end
62
64
 
63
- def self.list_order_values(klass)
64
- klass.respond_to?(:inline_forms_list) ? klass.inline_forms_list.order_values : []
65
+ def self.list_scope(klass)
66
+ klass.inline_forms_list if klass.respond_to?(:inline_forms_list)
67
+ end
68
+
69
+ # An association join becomes a LEFT join, so a record without the
70
+ # association stays in the list. A string join can't: it stays as written.
71
+ def self.with_list_joins(relation, list)
72
+ named, raw = list.joins_values.partition { |join| join.is_a?(Symbol) || join.is_a?(Hash) }
73
+ named = (named | list.left_outer_joins_values | list.eager_load_values) - relation.joins_values
74
+ raw -= relation.joins_values
75
+ relation = relation.left_outer_joins(*named) if named.any?
76
+ relation = relation.joins(*raw) if raw.any?
77
+ relation
65
78
  end
66
79
  end
67
80
  end
@@ -8,10 +8,17 @@ module InlineForms
8
8
  def pdf_link_show(object, attribute)
9
9
  # the attribute is the action
10
10
  # turbo:false so the links leave the field's turbo-frame: the preview has no matching frame ("Content missing") and the PDF is not HTML; the preview gets its own tab so the record stays open
11
- "#{link_to t("inline_forms.files.preview", :default => "preview"), "/#{attribute}/#{object.id}", :class => "pdf_preview", :target => "_blank", :data => { :turbo => false }} | #{link_to t("inline_forms.files.download_pdf", :default => "download pdf"), "/#{attribute}/#{object.id}.pdf", :class => "pdf_open", :data => { :turbo => false }}".html_safe
11
+ # script_name so the links work in an app deployed under a sub-path
12
+ path = "#{request.script_name}/#{attribute}/#{object.id}"
13
+ safe_join([
14
+ link_to(t("inline_forms.files.preview", :default => "preview"), path, :class => "pdf_preview", :target => "_blank", :data => { :turbo => false }),
15
+ link_to(t("inline_forms.files.download_pdf", :default => "download pdf"), "#{path}.pdf", :class => "pdf_open", :data => { :turbo => false })
16
+ ], " | ")
12
17
  end
13
18
 
14
19
  def pdf_link_edit(object, attribute)
20
+ # the attribute is an action: a model needs no method for it
21
+ return unless object.respond_to?(attribute)
15
22
  o = object.send(attribute)
16
23
  o = o.to_s + " (" + distance_of_time_in_words_to_now(o) + ")" if o.is_a?(Time)
17
24
  o = o._presentation if o.respond_to?(:_presentation)
@@ -19,11 +19,13 @@ module InlineForms
19
19
  end
20
20
 
21
21
  @inline_forms_searchable = true
22
- table = table_name
23
- clause = cols.map { |c|
24
- "#{connection.quote_table_name(table)}.#{connection.quote_column_name(c)} LIKE :q"
25
- }.join(" OR ")
22
+ # The clause is built when the scope runs, not here: quoting needs a
23
+ # database connection, and the class body runs whenever the model
24
+ # loads (eager loading, assets:precompile without a database).
26
25
  scope :inline_forms_search, ->(q) {
26
+ clause = cols.map { |c|
27
+ "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(c)} LIKE :q"
28
+ }.join(" OR ")
27
29
  where(clause, q: "%#{sanitize_sql_like(q.to_s)}%")
28
30
  }
29
31
  end
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  module InlineForms
4
- VERSION = "8.1.64"
4
+ VERSION = "8.1.66"
5
5
  end
@@ -1,10 +1,16 @@
1
1
  en:
2
2
  inline_forms:
3
- general:
4
- application_title: Application Title
3
+ # The generic list search (inline_forms/_generic_search), shown for
4
+ # models that declare inline_forms_search_on.
5
+ search:
6
+ placeholder: "search..."
7
+ submit: search
8
+ reset: reset
9
+ # general.application_title, devise.title_for_devise and devise.welcome
10
+ # are not defined here: without a host override the views use the
11
+ # host's application_name translation (InlineFormsHelper#inline_forms_application_title).
5
12
  devise:
6
- title_for_devise: Title for Devise
7
- welcome: Welcome for Devise
13
+ welcome_to: "Welcome to %{name}!"
8
14
  passwords:
9
15
  forgot_your_password: Forgot Your Password?
10
16
  change_your_password: Change your password, if you want!
@@ -152,3 +158,8 @@ en:
152
158
  messages:
153
159
  inline_forms_file_too_large: "is too large (maximum %{count} MB)"
154
160
  inline_forms_file_missing: "no file chosen"
161
+ # Version and file timestamps (InlineFormsHelper#inline_forms_time and the
162
+ # file trash): rails-i18n's :short has no year.
163
+ time:
164
+ formats:
165
+ inline_forms: "%d %b %Y %H:%M"
@@ -1,10 +1,16 @@
1
1
  nl:
2
2
  inline_forms:
3
- general:
4
- application_title: Application Title
3
+ # The generic list search (inline_forms/_generic_search), shown for
4
+ # models that declare inline_forms_search_on.
5
+ search:
6
+ placeholder: "zoeken..."
7
+ submit: zoek
8
+ reset: wissen
9
+ # general.application_title, devise.title_for_devise and devise.welcome
10
+ # are not defined here: without a host override the views use the
11
+ # host's application_name translation (InlineFormsHelper#inline_forms_application_title).
5
12
  devise:
6
- title_for_devise: Title for Devise
7
- welcome: Welcome for Devise
13
+ welcome_to: "Welkom bij %{name}!"
8
14
  passwords:
9
15
  forgot_your_password: Wachtwoord Vergeten?
10
16
  change_your_password: Verander wachtwoord, als u wilt!
@@ -152,3 +158,8 @@ nl:
152
158
  messages:
153
159
  inline_forms_file_too_large: "is te groot (maximaal %{count} MB)"
154
160
  inline_forms_file_missing: "geen bestand gekozen"
161
+ # Version and file timestamps (InlineFormsHelper#inline_forms_time and the
162
+ # file trash): rails-i18n's :short has no year.
163
+ time:
164
+ formats:
165
+ inline_forms: "%d %b %Y %H:%M"
@@ -15,6 +15,7 @@ class DropdownTest < InlineFormsIntegrationTestCase
15
15
  klass.singleton_class.remove_method(:inline_forms_list) if klass.singleton_class.method_defined?(:inline_forms_list, false)
16
16
  end
17
17
  Phase.remove_method(:disabled_for_dropdown?) if Phase.method_defined?(:disabled_for_dropdown?, false)
18
+ Kind.default_scopes = []
18
19
  end
19
20
 
20
21
  def view
@@ -90,6 +91,45 @@ class DropdownTest < InlineFormsIntegrationTestCase
90
91
  assert_equal %w[Alpha Hidden Zeta], html.scan(/<option value="([^"]*)">/).flatten
91
92
  end
92
93
 
94
+ test "a reorder in inline_forms_list replaces a default_scope order" do
95
+ Kind.send(:default_scope) { order(:name) }
96
+ %w[Alpha Otro Zeta].each { |name| Kind.create!(name: name) }
97
+ Kind.define_singleton_method(:inline_forms_list) { reorder(Arel::Nodes::Descending.new(arel_table[:name].eq("Otro")), :name) }
98
+
99
+ assert_equal %w[Otro Alpha Zeta], InlineForms::DropdownOrder.apply(Kind.all).map(&:name)
100
+ end
101
+
102
+ test "the list order follows a default_scope order, without repeating it" do
103
+ Kind.send(:default_scope) { order(:name) }
104
+ Kind.define_singleton_method(:inline_forms_list) { order(:id) }
105
+
106
+ sql = InlineForms::DropdownOrder.apply(Kind.all).to_sql
107
+
108
+ assert_equal 1, sql.scan('"kinds"."name"').size, sql
109
+ assert_operator sql.index('"kinds"."name"'), :<, sql.index('"kinds"."id"')
110
+ end
111
+
112
+ test "an inline_forms_list order on a joined table hides no records" do
113
+ Part.create!(name: "Axle", phase: Phase.create!(name: "z"))
114
+ Part.create!(name: "Bolt", phase: Phase.create!(name: "a"))
115
+ Part.create!(name: "Free")
116
+ Part.define_singleton_method(:inline_forms_list) { joins(:phase).order("phases.name DESC") }
117
+
118
+ assert_equal %w[Axle Bolt Free], InlineForms::DropdownOrder.apply(Part.all).map(&:name)
119
+ end
120
+
121
+ test "check_list keeps a joining inline_forms_list filter and its order" do
122
+ machine = Machine.create!(name: "Press")
123
+ Part.create!(name: "Axle", phase: Phase.create!(name: "z"))
124
+ Part.create!(name: "Bolt", phase: Phase.create!(name: "a"))
125
+ Part.create!(name: "Hidden")
126
+ Part.define_singleton_method(:inline_forms_list) { joins(:phase).order("phases.name") }
127
+
128
+ html = view.check_list_edit(machine, :parts)
129
+
130
+ assert_equal %w[Bolt Axle], html.scan(%r{<label for=[^>]*>([^<]*)</label>}).flatten.reject(&:empty?)
131
+ end
132
+
93
133
  test "disabled_for_dropdown? and the options come from one query" do
94
134
  create_phases
95
135
  Phase.define_method(:disabled_for_dropdown?) { name == "decision" }
@@ -62,6 +62,23 @@ class LegacyElementsTest < InlineFormsIntegrationTestCase
62
62
  assert_match %r{>preview</a> \| <a[^>]*>download pdf</a>}, response.body
63
63
  end
64
64
 
65
+ test "pdf_link needs no model method in the new form" do
66
+ assert_not Widget.new.respond_to?(:report)
67
+
68
+ get new_widget_path(update: "widgets_list"), headers: frame_headers("widgets_list")
69
+
70
+ assert_response :success
71
+ end
72
+
73
+ test "pdf_link links keep the app's sub-path" do
74
+ frame = "widget_#{@widget.id}"
75
+ get widget_path(@widget, update: frame), headers: frame_headers(frame), env: { "SCRIPT_NAME" => "/portal" }
76
+
77
+ assert_response :success
78
+ assert_select %(a.pdf_preview[href="/portal/report/#{@widget.id}"])
79
+ assert_select %(a.pdf_open[href="/portal/report/#{@widget.id}.pdf"])
80
+ end
81
+
65
82
  test "pdf_link links leave the field's turbo frame" do
66
83
  frame = "widget_#{@widget.id}"
67
84
  get widget_path(@widget, update: frame), headers: frame_headers(frame)
@@ -19,6 +19,8 @@ class LocaleTest < InlineFormsIntegrationTestCase
19
19
  inline_forms.view.version_events.update inline_forms.view.version_events.destroy
20
20
  inline_forms.devise.login common.more common.logout inline_forms.view.create_failed
21
21
  inline_forms.files.preview inline_forms.files.download_pdf
22
+ inline_forms.search.placeholder inline_forms.search.submit inline_forms.search.reset
23
+ inline_forms.devise.welcome_to time.formats.inline_forms
22
24
  ].freeze
23
25
 
24
26
  setup do
@@ -107,7 +109,60 @@ class LocaleTest < InlineFormsIntegrationTestCase
107
109
  end
108
110
  refute_includes body, "<strong>Event</strong>"
109
111
  version = @machine.versions.last
110
- assert_includes body, ERB::Util.h(I18n.l(version.created_at, format: :short, locale: :nl))
112
+ assert_includes body, ERB::Util.h(I18n.l(version.created_at, format: :inline_forms, locale: :nl))
113
+ end
114
+
115
+ # rails-i18n's :short ("%d %b %H:%M") has no year: edits years apart read
116
+ # the same next to Restore.
117
+ test "version timestamps show the year, and the exact time on hover" do
118
+ version = @machine.versions.last
119
+ version.update_columns(created_at: Time.zone.local(2019, 3, 5, 14, 7))
120
+ row = "machine_#{@machine.id}"
121
+ strictly(:nl) do
122
+ get list_versions_machine_path(@machine, update: row), headers: frame_headers(row)
123
+ end
124
+
125
+ assert_response :success
126
+ iso = version.reload.created_at.iso8601
127
+ assert_includes response.body, %(<time datetime="#{iso}" title="#{iso}">05 mrt 2019 14:07</time>)
128
+ end
129
+
130
+ test "the generic search box is translated" do
131
+ I18n.backend.store_translations(:en, widgets: "widgets")
132
+ I18n.backend.store_translations(:nl, widgets: "widgets")
133
+ { en: [ "search...", "search", "reset" ], nl: [ "zoeken...", "zoek", "wissen" ] }.each do |locale, (placeholder, submit, reset)|
134
+ strictly(locale) { get widgets_path }
135
+
136
+ assert_response :success
137
+ assert_includes response.body, %(placeholder="#{placeholder}")
138
+ assert_includes response.body, %(value="#{submit}")
139
+ assert_includes response.body, %(title="#{reset}")
140
+ refute_includes response.body, "zoek op naam" if locale == :en
141
+ end
142
+ end
143
+
144
+ # The gem shipped "Application Title" / "Title for Devise" / "Welcome for
145
+ # Devise" in en and nl; any locale the host did not override showed them.
146
+ test "no placeholder titles: the gem's titles use the host's application name" do
147
+ %i[en nl].each do |locale|
148
+ %w[inline_forms.general.application_title inline_forms.devise.title_for_devise inline_forms.devise.welcome].each do |key|
149
+ refute I18n.exists?(key, locale), "#{locale}.#{key} is a placeholder; the views fall back to application_name"
150
+ end
151
+ end
152
+
153
+ get widgets_path
154
+ I18n.backend.store_translations(:nl, application_name: "Werkplaats")
155
+ strictly(:nl) do
156
+ devise = controller.render_to_string(template: "stats/show", layout: "devise")
157
+ assert_includes devise, "Werkplaats v#{InlineForms::VERSION}"
158
+ top_bar = controller.render_to_string(partial: "devise/sessions/top-bar")
159
+ assert_includes top_bar, "Werkplaats #{InlineForms::VERSION}"
160
+ assert_equal "Welkom bij Werkplaats!",
161
+ I18n.t("inline_forms.devise.welcome", default: I18n.t("inline_forms.devise.welcome_to", name: "Werkplaats"))
162
+ end
163
+ I18n.with_locale(:en) do
164
+ assert_includes controller.render_to_string(template: "stats/show", layout: "application"), "Dummy v#{InlineForms::VERSION}"
165
+ end
111
166
  end
112
167
 
113
168
  test "the versions list in en keeps its English labels" do
@@ -123,11 +178,11 @@ class LocaleTest < InlineFormsIntegrationTestCase
123
178
  end
124
179
 
125
180
  test "pdf_link reads Dutch" do
181
+ original_default = I18n.default_locale
126
182
  widget = Widget.create!(name: "Rapport")
127
183
  frame = "widget_#{widget.id}"
128
184
 
129
185
  # Not strictly: the dummy's dropdown values have no nl texts.
130
- original_default = I18n.default_locale
131
186
  I18n.with_locale(:nl) do
132
187
  I18n.default_locale = :nl
133
188
  get widget_path(widget, update: frame), headers: frame_headers(frame)
@@ -62,4 +62,22 @@ class ModelTopBarTest < InlineFormsIntegrationTestCase
62
62
  refute_includes response.body, "new_button"
63
63
  refute_includes response.body, %(id="input_search")
64
64
  end
65
+
66
+ # The class body runs whenever the model loads (eager loading,
67
+ # assets:precompile with no database); quoting there needed a connection.
68
+ test "inline_forms_search_on needs no database connection until the search runs" do
69
+ model = Class.new(ApplicationRecord) do
70
+ self.table_name = "widgets"
71
+ def self.name = "OfflineWidget"
72
+ end
73
+ no_db = ->(*) { raise "database touched at declaration" }
74
+ %i[connection lease_connection with_connection].each { |m| model.define_singleton_method(m, &no_db) }
75
+
76
+ model.inline_forms_search_on :name
77
+
78
+ assert model.inline_forms_searchable?
79
+ %i[connection lease_connection with_connection].each { |m| model.singleton_class.remove_method(m) }
80
+ Widget.create!(name: "Gamma")
81
+ assert_equal [ "Gamma" ], model.inline_forms_search("amm").pluck(:name)
82
+ end
65
83
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration_test_helper"
4
+
5
+ # Top-bar submenus after a Turbo Drive visit. Turbo replaced <body> and re-ran
6
+ # the inline_forms bundle (a fresh jQuery, its own Foundation plugin data),
7
+ # and until that finished the new body's submenus showed as plain lists;
8
+ # StProject, Scouting and Papiamentu each carried the same layout copy to
9
+ # hide them (html.no-js, a turbo:load re-init, a CSS rule). Now the gem does.
10
+ class TurboMenuTest < InlineFormsIntegrationTestCase
11
+ ROOT = File.expand_path("../..", __dir__)
12
+
13
+ test "the layout starts as no-js" do
14
+ get widgets_path
15
+
16
+ assert_response :success
17
+ assert_match %r{<html lang="en" class="no-js">}, response.body
18
+ # Not data-turbo-eval="false": a Turbo visit from a page whose layout
19
+ # lacks the bundle has to run it.
20
+ assert_match %r{<script src="[^"]*/inline_forms/inline_forms[^"]*\.js"></script>}, response.body
21
+ end
22
+
23
+ test "inline_forms.js boots once and re-initialises Foundation on turbo:load" do
24
+ js = File.read(File.join(ROOT, "app/assets/javascripts/inline_forms/inline_forms.js"))
25
+ boot = js[/if \(!window\.inlineFormsBooted\) \{.*?\n\}\n/m]
26
+
27
+ assert boot, "the DOM-ready and turbo:load wiring sits behind the once-guard"
28
+ turbo_load = boot[/addEventListener\("turbo:load".*?\n \}\);/m]
29
+ assert_includes turbo_load, "window.jQuery(document).foundation();"
30
+ assert_includes turbo_load, "initInlineFormsWidgets(document);"
31
+ end
32
+
33
+ test "uninitialised submenus stay hidden after a visit" do
34
+ scss = File.read(File.join(ROOT, "app/assets/stylesheets/inline_forms/inline_forms.scss"))
35
+
36
+ assert_match(/^\.dropdown\.menu > li > ul\.menu:not\(\.is-dropdown-submenu\) \{\n display: none;\n\}/, scss)
37
+ end
38
+
39
+ # Compiled CSS is checked in the example app (example_app_stylesheet_test).
40
+ test "tippy.css is inlined, not left as a plain CSS @import the browser 404s on" do
41
+ scss = File.read(File.join(ROOT, "app/assets/stylesheets/inline_forms/foundation_and_overrides.scss"))
42
+
43
+ refute_match(/^\s*@import\s+['"][^'"]*tippy\.css['"]/, scss)
44
+ assert_includes scss, "@include meta.load-css('../../../vendor/assets/stylesheets/tippy');"
45
+ end
46
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inline_forms
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.64
4
+ version: 8.1.66
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -334,6 +334,7 @@ files:
334
334
  - test/integration/schema_batch_pipeline_test.rb
335
335
  - test/integration/schema_edit_test.rb
336
336
  - test/integration/schema_preview_test.rb
337
+ - test/integration/turbo_menu_test.rb
337
338
  - test/integration_test_helper.rb
338
339
  - test/plain_text_configuration_test.rb
339
340
  - test/schema_apply_test.rb
@@ -424,6 +425,7 @@ test_files:
424
425
  - test/integration/schema_batch_pipeline_test.rb
425
426
  - test/integration/schema_edit_test.rb
426
427
  - test/integration/schema_preview_test.rb
428
+ - test/integration/turbo_menu_test.rb
427
429
  - test/integration_test_helper.rb
428
430
  - test/plain_text_configuration_test.rb
429
431
  - test/schema_apply_test.rb