inline_forms 8.1.66 → 8.1.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d798e9e7a6d981b422009a82073f77675148cfcd17263b501067d7d6e0a3b57
4
- data.tar.gz: 76876893fbad74dd364b02c21f927bc414cb86b2b6e7a89b1caa5a3a941bab4d
3
+ metadata.gz: faafc3188dc88f06ee65cc379452eb7dde62753fe63b488b0250841c4cdb641d
4
+ data.tar.gz: 880f68f083664c8bf6de77fb522577365c43069523da92ca42a01bdd2fa7378a
5
5
  SHA512:
6
- metadata.gz: d1c4624d5315833d86d2578112d2f494b8f7cd32219698de060afbc7570a638fb98c5bf07fd376de87854b264c85521033da2c88920093c3552b5925c5062c8b
7
- data.tar.gz: dab2278a87d37e9005baafc360503a0a767cbd95ce1e20ad0484463098d65763547daac3f25c6e16ca1707ca8ee92ef7e9f922ffdfce996c74670af849f028bc
6
+ metadata.gz: e378f136a965b95f21f0e09e55776b1019eb3fa8a3ada7c1117ab101f2f2a117d654ebfe1b3cbf2c02b01cfd01c7e2096de3cf3d00ffa1d73b6c5474b0b81c3d
7
+ data.tar.gz: 86d6a6d5553a2b98227a3367b0f8cbdc703646bfa01b5a5f55a59dcf798a1c9bf029a0dfcfe7506d2a18e0eaf960155743266e146e4b9ab62d5678ac45b1bf51
data/CHANGELOG.md CHANGED
@@ -8,6 +8,36 @@ All notable changes to this project are documented in this file.
8
8
 
9
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
10
 
11
+ ## [8.1.67] - 2026-11-06
12
+
13
+ Fixes from a code review of 8.1.66.
14
+
15
+ ### Fixed
16
+
17
+ - **Version and file trash pages no longer crash in a locale without `time.formats.inline_forms`.** 8.1.66 defined the format in en and nl only, and a bare `l(time, format: :inline_forms)` raises `I18n::MissingTranslationData` in any other locale when the host has no i18n fallbacks (rails-i18n's `:short`, used before, exists in every locale). The versions list, the file entries and the file trash now go through `inline_forms_time_text`, which passes the same format (`InlineForms::TIME_FORMAT`) as the default; the month names still come from the locale.
18
+ - **No more document listeners piling up on Turbo visits.** 8.1.66's once-guard in `inline_forms.js` left the `turbo:frame-load` listener and the `.translation_missing` mouseover handler outside it, so a layout that re-runs the bundle added another of each per visit (after 10 visits every frame load rebuilt each tooltip 11 times). Both are inside the guard now (the mouseover handler is a plain DOM listener).
19
+ - **Foundation and the widgets initialise once per page.** The first page ran `foundation()` and `initInlineFormsWidgets` from both the ready event and `turbo:load`, which destroyed and rebuilt every tooltip; one boot function now runs once per `<body>`.
20
+ - **The submenu rule no longer hides drilldown submenus.** 8.1.66 hid every `.dropdown.menu` submenu without `is-dropdown-submenu`, which beats Foundation's own `.is-drilldown-submenu` rules for a `data-responsive-menu` on small screens. It now applies to `[data-dropdown-menu]` menus only.
21
+ - **The title keys exist again, and say the application's name.** 8.1.66 removed `inline_forms.general.application_title`, `inline_forms.devise.title_for_devise` and `inline_forms.devise.welcome`, so a host view calling one bare (Papiamentu has several) showed "translation missing" in any locale it did not translate and that had no fallbacks. In en and nl they are now lambdas (`lib/locales/inline_forms_titles.rb`) that return `InlineForms.application_title`; a host's own translation still wins (its locale files load after the gem's; the example app checks the order).
22
+ - **One source for the application's name.** `InlineForms.application_title` is the host's `application_name` translation, else its `ApplicationHelper#application_name` (8.1.66 skipped that and used the titleized module name, so an app could say "MyApp" in en and "My App" in nl), else the Rails module name. `inline_forms_application_title`, the admin layout's `<title>` and the Devise header use it.
23
+
24
+ ### Changed
25
+
26
+ - **The admin layout loads Popper, Tippy and `inline_forms.js` in `<head>`.** In `<body>` Turbo Drive re-ran them on every visit with a fresh jQuery, the cause of 8.1.66's menu flash; Turbo runs head scripts once and appends missing ones in order on a visit from another layout. `no-js`, the once-guard and the submenu rule stay for host layouts that still load the bundle in `<body>`.
27
+ - **Installer: the generated `config/locales/inline_forms_local.en.yml` has only `application_name`.** It also wrote the three title keys, in en only.
28
+ - **`inline_forms_search_on` builds its clause once,** on the first search (`inline_forms_search_clause`), instead of on every search.
29
+
30
+ ### Tests
31
+
32
+ - `turbo_menu_test`: the scripts in `<head>` in order, no document listener outside the guard, one boot per body, the `[data-dropdown-menu]` rule.
33
+ - `locale_test`: the title keys resolve to the app's name in en and nl (translation, then the helper), a host translation wins, timestamps render in a locale without the gem's format.
34
+ - `model_top_bar_test`: the search clause is built once.
35
+ - Example app: `example_app_layout_title_test` checks the host's locale files load after the gem's title keys and that the keys say the app name in en and nl; `example_app_stylesheet_test` expects the scoped rule.
36
+
37
+ ### Lockstep
38
+
39
+ - validation_hints 8.1.67, inline_forms_installer 8.1.67, inline_forms_schema_edit 8.1.67.
40
+
11
41
  ## [8.1.66] - 2026-11-06
12
42
 
13
43
  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,28 +14,41 @@
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
- // 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.
17
+ // Listeners are added once per page load. The gem's layouts load this bundle
18
+ // in <head>, which Turbo Drive keeps across visits, but a host layout may
19
+ // still have it in <body>, which Turbo re-runs on every visit (each run with a
20
+ // fresh jQuery): without the guard every run would add another set.
21
21
  if (!window.inlineFormsBooted) {
22
22
  window.inlineFormsBooted = true;
23
23
 
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() {
24
+ // Foundation and the widgets, once per <body>. Turbo Drive replaces <body>
25
+ // on a visit, and the new top bar's DropdownMenu needs initialising or its
26
+ // submenus show as plain lists. The first page gets both the ready event
27
+ // and turbo:load (and a layout without Turbo only the first), so the body
28
+ // check keeps it to one init. jQuery is looked up at call time.
29
+ var inlineFormsBootedBody = null;
30
+ var bootInlineFormsPage = function() {
31
+ if (!document.body || document.body === inlineFormsBootedBody) { return; }
32
+ inlineFormsBootedBody = document.body;
35
33
  if (window.jQuery && window.jQuery.fn.foundation) {
36
34
  window.jQuery(document).foundation();
37
35
  }
38
36
  initInlineFormsWidgets(document);
37
+ };
38
+ $(bootInlineFormsPage);
39
+ document.addEventListener("turbo:load", bootInlineFormsPage);
40
+
41
+ // Re-bind widgets (tooltips) after Turbo Frame swaps.
42
+ document.addEventListener("turbo:frame-load", function(event) {
43
+ var root = event.target;
44
+ if (!root || !root.querySelectorAll) { return; }
45
+ initInlineFormsWidgets(root);
46
+ });
47
+
48
+ // get rid of translation_missing tooltips
49
+ document.addEventListener("mouseover", function(event) {
50
+ var el = event.target.closest && event.target.closest(".translation_missing");
51
+ if (el) { el.setAttribute("title", ""); }
39
52
  });
40
53
  }
41
54
 
@@ -104,17 +117,3 @@ function initValidationHintTooltips(root) {
104
117
  }
105
118
  });
106
119
  }
107
-
108
- // get rid of translation_missing tooltips
109
- $(document).ready(function() {
110
- $(this).on('mouseover', '.translation_missing', function() {
111
- $(this).attr('title', '');
112
- });
113
- });
114
-
115
- // Re-bind widgets (tooltips) after Turbo Frame swaps.
116
- document.addEventListener("turbo:frame-load", function(event) {
117
- var root = event.target;
118
- if (!root || !root.querySelectorAll) { return; }
119
- initInlineFormsWidgets(root);
120
- });
@@ -117,9 +117,12 @@
117
117
  // marks it `is-dropdown-submenu` (whose own rules then hide it). On the first
118
118
  // page `html.no-js` covers that gap (layouts/inline_forms.html.erb), but
119
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) {
120
+ // visit the new body's submenus could flash open until inline_forms.js
121
+ // re-initialised them on turbo:load (most of all from a host layout that
122
+ // still loads the bundle in <body>). Only for menus marked
123
+ // `data-dropdown-menu`: a `data-responsive-menu` turns its submenus into
124
+ // `is-drilldown-submenu` on small screens, which this rule would hide.
125
+ .dropdown.menu[data-dropdown-menu] > li > ul.menu:not(.is-dropdown-submenu) {
123
126
  display: none;
124
127
  }
125
128
 
@@ -398,15 +398,21 @@ module InlineFormsHelper
398
398
  # (time.formats.inline_forms; rails-i18n's :short has none, so edits years
399
399
  # apart read the same), the exact ISO 8601 time on hover.
400
400
  def inline_forms_time(time)
401
- tag.time(l(time, format: :inline_forms), datetime: time.iso8601, title: time.iso8601)
401
+ tag.time(inline_forms_time_text(time), datetime: time.iso8601, title: time.iso8601)
402
402
  end
403
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).
404
+ # The text alone. The gem defines time.formats.inline_forms only in en and
405
+ # nl; in another locale without it (and without i18n fallbacks) a bare
406
+ # l(format: :inline_forms) raises, so the same format is the default. The
407
+ # month names still come from that locale (rails-i18n).
408
+ def inline_forms_time_text(time)
409
+ l(time, format: :inline_forms, default: InlineForms::TIME_FORMAT)
410
+ end
411
+
412
+ # The application's name for the gem's titles and the Devise pages
413
+ # (InlineForms.application_title).
408
414
  def inline_forms_application_title
409
- t("application_name", default: Rails.application.class.module_parent_name.titleize)
415
+ InlineForms.application_title
410
416
  end
411
417
 
412
418
  def version_modified_by(id)
@@ -1,7 +1,7 @@
1
1
  <%# Unused by the gem's own devise views; kept for host apps that render it. %>
2
2
  <%# Same title source as layouts/inline_forms; Devise 5 dropped %>
3
3
  <%# devise_error_messages!, so errors use Devise's own error_messages partial. %>
4
- <div id="large_title"><%= t('application_name', default: application_name) %></div>
4
+ <div id="large_title"><%= inline_forms_application_title %></div>
5
5
  <div id="flash_message">
6
6
  <% if resource.errors.any? %>
7
7
  <%= render "devise/shared/error_messages", resource: resource %>
@@ -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: :inline_forms), count: @last_sweep.purged_count) %>
17
+ <%= t("inline_forms.files.global.sweep_last", at: inline_forms_time_text(@last_sweep.ran_at), 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: :inline_forms)) %></td>
81
+ <td><%= t("inline_forms.files.by_on", user: inline_forms_file_user_name(entry.trashed_by_id), at: inline_forms_time_text(entry.trashed_at)) %></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: :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)) %>
83
+ <% if entry.state == :restored %><%= t("inline_forms.files.state_restored", user: inline_forms_file_user_name(entry.restored_by_id), at: inline_forms_time_text(entry.restored_at)) %>
84
+ <% elsif entry.state == :purged %><%= t("inline_forms.files.state_purged", user: inline_forms_file_user_name(entry.purged_by_id), at: inline_forms_time_text(entry.purged_at)) %>
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: :inline_forms)) %>
44
+ <%= t("inline_forms.files.by_on", user: inline_forms_file_user_name(entry.trashed_by_id), at: inline_forms_time_text(entry.trashed_at)) %>
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: :inline_forms)) %>
48
+ <%= t("inline_forms.files.state_restored", user: inline_forms_file_user_name(entry.restored_by_id), at: inline_forms_time_text(entry.restored_at)) %>
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: :inline_forms)) %>
50
+ <%= t("inline_forms.files.state_purged", user: inline_forms_file_user_name(entry.purged_by_id), at: inline_forms_time_text(entry.purged_at)) %>
51
51
  <% elsif entry.state == :expired %>
52
52
  <%= t("inline_forms.files.state_expired") %>
53
53
  <% else %>
@@ -1,18 +1,17 @@
1
1
  <!DOCTYPE html>
2
2
  <%# class="no-js": Foundation's CSS hides the top-bar submenus while it is set %>
3
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. %>
4
+ <%# plain lists before then. Turbo Drive never puts it back; after a visit %>
5
+ <%# inline_forms.js initialises the new body on turbo:load, and the %>
6
+ <%# `[data-dropdown-menu]` rule in inline_forms.scss covers any gap. %>
7
7
  <html lang="<%= I18n.locale %>" class="no-js">
8
8
  <head>
9
9
  <meta charset="utf-8" />
10
10
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11
11
  <%# A missing translation must not reach <title>: a bare t() returns an %>
12
12
  <%# html_safe <span class="translation_missing"> that shows up as markup. %>
13
- <%# The `default:` (the host's ApplicationHelper#application_name, which %>
14
- <%# the header partial needs anyway) makes t() return plain, escaped text. %>
15
- <title><%= t('application_name', default: application_name) %> v<%= InlineForms::VERSION %></title>
13
+ <%# inline_forms_application_title always has a plain-text fallback. %>
14
+ <title><%= inline_forms_application_title %> v<%= InlineForms::VERSION %></title>
16
15
  <%= stylesheet_link_tag "application" %>
17
16
  <%= stylesheet_link_tag "inline_forms_fonts" %>
18
17
  <%= stylesheet_link_tag "inline_forms/inline_forms" %>
@@ -22,6 +21,16 @@
22
21
  <%# Publishes the CSP nonce (nothing without a nonce generator) for the %>
23
22
  <%# scripts Turbo inserts. %>
24
23
  <%= csp_meta_tag %>
24
+ <%# In <head>, so Turbo Drive runs them once: it keeps head scripts across %>
25
+ <%# visits (a body script runs again on every visit, with a fresh jQuery), %>
26
+ <%# and appends them, in order, on a visit from a page that lacks them. %>
27
+ <%# Tippy.js (v6) needs Popper.js as a global before its UMD factory runs %>
28
+ <%# (`tippy-bundle.umd.min` only bundles default themes, not Popper itself). %>
29
+ <%# inline_forms.js initialises Foundation on the ready event and on %>
30
+ <%# turbo:load, once per <body>. %>
31
+ <%= javascript_include_tag 'popper.min' %>
32
+ <%= javascript_include_tag 'tippy-bundle.umd.min' %>
33
+ <%= javascript_include_tag 'inline_forms/inline_forms' %>
25
34
  </head>
26
35
 
27
36
  <body class="<%= inline_forms_body_theme_class %>">
@@ -40,14 +49,6 @@
40
49
  <div id="outer_container">
41
50
  <%= yield %>
42
51
  </div>
43
- <%# Tippy.js (v6) needs Popper.js as a global before its UMD factory runs %>
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. %>
48
- <%= javascript_include_tag 'popper.min' %>
49
- <%= javascript_include_tag 'tippy-bundle.umd.min' %>
50
- <%= javascript_include_tag 'inline_forms/inline_forms' %>
51
52
  <%# Turbo (Hotwire) is loaded as an ES module so it does not collide with %>
52
53
  <%# the classic-script Sprockets bundle above. Drive stays at the default %>
53
54
  <%# (enabled); inline interactions use `<turbo-frame>` + HTML responses. %>
@@ -19,16 +19,21 @@ module InlineForms
19
19
  end
20
20
 
21
21
  @inline_forms_searchable = true
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).
22
+ @inline_forms_search_columns = cols
23
+ model = self
25
24
  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 ")
29
- where(clause, q: "%#{sanitize_sql_like(q.to_s)}%")
25
+ where(model.inline_forms_search_clause, q: "%#{sanitize_sql_like(q.to_s)}%")
30
26
  }
31
27
  end
28
+
29
+ # Built on the first search, not in inline_forms_search_on: quoting
30
+ # needs a database connection, and the class body runs whenever the
31
+ # model loads (eager loading, assets:precompile without a database).
32
+ def inline_forms_search_clause
33
+ @inline_forms_search_clause ||= @inline_forms_search_columns.map { |c|
34
+ "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(c)} LIKE :q"
35
+ }.join(" OR ")
36
+ end
32
37
  end
33
38
  end
34
39
  end
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  module InlineForms
4
- VERSION = "8.1.66"
4
+ VERSION = "8.1.67"
5
5
  end
data/lib/inline_forms.rb CHANGED
@@ -187,6 +187,28 @@ module InlineForms
187
187
  # programmatically (generator / GUI); the user edits it to real values after.
188
188
  PLACEHOLDER_VALUES = { 1 => "one", 2 => "two" }.freeze
189
189
 
190
+ # Version and file timestamps: time.formats.inline_forms in the gem's en
191
+ # and nl locales, and the default where a locale lacks it.
192
+ TIME_FORMAT = "%d %b %Y %H:%M"
193
+
194
+ # The application's name for the gem's titles and the Devise pages: the
195
+ # host's `application_name` translation, else its
196
+ # ApplicationHelper#application_name (the installer writes both, and the
197
+ # gem's header partial uses the helper), else its Rails module name. The
198
+ # gem's title keys (inline_forms.general.application_title,
199
+ # inline_forms.devise.title_for_devise and .welcome) resolve to this in en
200
+ # and nl unless the host translates them (lib/locales/inline_forms_titles.rb).
201
+ def self.application_title
202
+ I18n.t("application_name", default: application_name_from_helper || Rails.application.class.module_parent_name)
203
+ end
204
+
205
+ def self.application_name_from_helper
206
+ return unless defined?(::ApplicationController)
207
+
208
+ helpers = ::ApplicationController.helpers
209
+ helpers.application_name if helpers.respond_to?(:application_name)
210
+ end
211
+
190
212
  def self.plain_text_form_element?(form_element)
191
213
  PLAIN_TEXT_FORM_ELEMENTS.include?(form_element.to_sym)
192
214
  rescue NoMethodError
@@ -408,7 +430,8 @@ module InlineForms
408
430
  end
409
431
  end
410
432
 
411
- I18n.load_path << Dir[File.join(File.expand_path(File.dirname(__FILE__) + "/locales"), "*.yml")]
433
+ # .rb too: inline_forms_titles.rb holds the title keys as lambdas.
434
+ I18n.load_path << Dir[File.join(File.expand_path(File.dirname(__FILE__) + "/locales"), "*.{yml,rb}")]
412
435
  I18n.load_path.flatten!
413
436
  end
414
437
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The gem's title keys, resolved to the application's name
4
+ # (InlineForms.application_title) instead of a placeholder. Lambdas, so a
5
+ # host view that calls a key bare gets the real name too. A host translation
6
+ # of the same key still wins: the host's locale files load after the gem's.
7
+ title = ->(_key, **_options) { InlineForms.application_title }
8
+ welcome = ->(_key, **_options) { I18n.t("inline_forms.devise.welcome_to", name: InlineForms.application_title) }
9
+ keys = {
10
+ inline_forms: {
11
+ general: { application_title: title },
12
+ devise: { title_for_devise: title, welcome: welcome }
13
+ }
14
+ }
15
+
16
+ { en: keys, nl: keys }
@@ -143,26 +143,56 @@ class LocaleTest < InlineFormsIntegrationTestCase
143
143
 
144
144
  # The gem shipped "Application Title" / "Title for Devise" / "Welcome for
145
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
-
146
+ # The keys now resolve to the application's name, also when a host view
147
+ # calls them bare.
148
+ test "the gem's title keys resolve to the application's name" do
153
149
  get widgets_path
154
150
  I18n.backend.store_translations(:nl, application_name: "Werkplaats")
155
151
  strictly(:nl) do
152
+ assert_equal "Werkplaats", I18n.t("inline_forms.general.application_title")
153
+ assert_equal "Werkplaats", I18n.t("inline_forms.devise.title_for_devise")
154
+ assert_equal "Welkom bij Werkplaats!", I18n.t("inline_forms.devise.welcome")
156
155
  devise = controller.render_to_string(template: "stats/show", layout: "devise")
157
156
  assert_includes devise, "Werkplaats v#{InlineForms::VERSION}"
158
157
  top_bar = controller.render_to_string(partial: "devise/sessions/top-bar")
159
158
  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
159
  end
163
160
  I18n.with_locale(:en) do
161
+ # No en application_name translation here: the host's
162
+ # ApplicationHelper#application_name.
163
+ assert_equal "Dummy", I18n.t("inline_forms.general.application_title")
164
+ assert_equal "Welcome to Dummy!", I18n.t("inline_forms.devise.welcome")
164
165
  assert_includes controller.render_to_string(template: "stats/show", layout: "application"), "Dummy v#{InlineForms::VERSION}"
165
166
  end
167
+ ensure
168
+ I18n.backend.reload!
169
+ end
170
+
171
+ test "a host translation of a title key wins" do
172
+ I18n.t("inline_forms.general.application_title") # loads the gem's files first, as a host app does
173
+ I18n.backend.store_translations(:nl, inline_forms: { general: { application_title: "Banko" } })
174
+
175
+ assert_equal "Banko", I18n.t("inline_forms.general.application_title", locale: :nl)
176
+ ensure
177
+ I18n.backend.reload!
178
+ end
179
+
180
+ # time.formats.inline_forms is defined in en and nl only; a bare
181
+ # l(format: :inline_forms) raises in any other locale without fallbacks.
182
+ test "version and trash timestamps render in a locale without the gem's format" do
183
+ original = I18n.available_locales
184
+ I18n.available_locales = original + [ :de ]
185
+ months = %w[Jan Feb Mär Apr Mai Jun Jul Aug Sep Okt Nov Dez]
186
+ I18n.backend.store_translations(:de, date: { abbr_month_names: [ nil, *months ] })
187
+ time = Time.zone.local(2019, 3, 5, 14, 7)
188
+
189
+ I18n.with_locale(:de) do
190
+ assert_raises(I18n::MissingTranslationData) { I18n.l(time, format: :inline_forms, raise: true) }
191
+ assert_equal "05 Mär 2019 14:07", ApplicationController.helpers.send(:inline_forms_time_text, time)
192
+ end
193
+ ensure
194
+ I18n.available_locales = original
195
+ I18n.backend.reload!
166
196
  end
167
197
 
168
198
  test "the versions list in en keeps its English labels" do
@@ -79,5 +79,7 @@ class ModelTopBarTest < InlineFormsIntegrationTestCase
79
79
  %i[connection lease_connection with_connection].each { |m| model.singleton_class.remove_method(m) }
80
80
  Widget.create!(name: "Gamma")
81
81
  assert_equal [ "Gamma" ], model.inline_forms_search("amm").pluck(:name)
82
+ # Built once, on the first search.
83
+ assert_same model.inline_forms_search_clause, model.inline_forms_search_clause
82
84
  end
83
85
  end
@@ -2,38 +2,52 @@
2
2
 
3
3
  require_relative "../integration_test_helper"
4
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.
5
+ # Top-bar submenus after a Turbo Drive visit. The bundle sat in <body>, so
6
+ # Turbo re-ran it on every visit (a fresh jQuery, its own Foundation plugin
7
+ # data), and until that finished the new body's submenus showed as plain
8
+ # lists; StProject, Scouting and Papiamentu each carried the same layout copy
9
+ # to hide them (html.no-js, a turbo:load re-init, a CSS rule). Now the gem
10
+ # loads the bundle in <head>, which Turbo runs once.
10
11
  class TurboMenuTest < InlineFormsIntegrationTestCase
11
12
  ROOT = File.expand_path("../..", __dir__)
12
13
 
13
- test "the layout starts as no-js" do
14
+ def boot_block
15
+ js = File.read(File.join(ROOT, "app/assets/javascripts/inline_forms/inline_forms.js"))
16
+ js[/^if \(!window\.inlineFormsBooted\) \{.*?\n\}\n/m]
17
+ end
18
+
19
+ test "the layout starts as no-js and loads the bundle in head" do
14
20
  get widgets_path
15
21
 
16
22
  assert_response :success
17
23
  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
24
+ head = response.body[%r{<head>.*</head>}m]
25
+ scripts = %w[popper.min tippy-bundle.umd.min inline_forms/inline_forms].map { |name|
26
+ head.index(%r{<script src="[^"]*/#{Regexp.escape(name)}[^"]*\.js"></script>})
27
+ }
28
+ assert scripts.all?, "popper, tippy and inline_forms.js are in <head>"
29
+ assert_equal scripts.sort, scripts, "in that order"
30
+ refute_match %r{<body.*inline_forms/inline_forms[^"]*\.js}m, response.body
21
31
  end
22
32
 
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);"
33
+ test "inline_forms.js wires every document listener once, and boots once per body" do
34
+ boot = boot_block
35
+
36
+ assert boot, "the listeners sit behind the once-guard"
37
+ rest = File.read(File.join(ROOT, "app/assets/javascripts/inline_forms/inline_forms.js")).sub(boot, "")
38
+ refute_match(/addEventListener|\$\(document\)|\.ready\(/, rest.gsub(%r{^\s*//.*$}, "").gsub(/el\.addEventListener\("input"/, ""),
39
+ "no document listener outside the guard: a host layout with the bundle in <body> re-runs it on every visit")
40
+ %w[turbo:load turbo:frame-load mouseover].each do |event|
41
+ assert_includes boot, "document.addEventListener(\"#{event}\""
42
+ end
43
+ assert_includes boot, "document.body === inlineFormsBootedBody"
44
+ assert_includes boot, "window.jQuery(document).foundation();"
31
45
  end
32
46
 
33
- test "uninitialised submenus stay hidden after a visit" do
47
+ test "uninitialised dropdown submenus stay hidden, drilldown menus are left alone" do
34
48
  scss = File.read(File.join(ROOT, "app/assets/stylesheets/inline_forms/inline_forms.scss"))
35
49
 
36
- assert_match(/^\.dropdown\.menu > li > ul\.menu:not\(\.is-dropdown-submenu\) \{\n display: none;\n\}/, scss)
50
+ assert_match(/^\.dropdown\.menu\[data-dropdown-menu\] > li > ul\.menu:not\(\.is-dropdown-submenu\) \{\n display: none;\n\}/, scss)
37
51
  end
38
52
 
39
53
  # Compiled CSS is checked in the example app (example_app_stylesheet_test).
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.66
4
+ version: 8.1.67
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -273,6 +273,7 @@ files:
273
273
  - lib/inline_forms/version.rb
274
274
  - lib/locales/inline_forms.en.yml
275
275
  - lib/locales/inline_forms.nl.yml
276
+ - lib/locales/inline_forms_titles.rb
276
277
  - lib/otherstuff/add_roles.sql
277
278
  - lib/otherstuff/mkrole
278
279
  - lib/otherstuff/roles_users.sql