hibiki_rails 0.12.0 → 0.14.0
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 +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +2 -2
- data/lib/generators/hibiki/rails/multiselect_injections.rb +1 -1
- data/lib/generators/hibiki/rails/scaffold/USAGE +3 -2
- data/lib/generators/hibiki/rails/scaffold_controller/USAGE +1 -1
- data/lib/generators/hibiki/rails/scaffold_controller/scaffold_controller_generator.rb +1 -1
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/list.rb.tt +13 -9
- data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_list.html.erb.tt +17 -14
- data/lib/generators/hibiki/rails/scaffold_helpers.rb +13 -6
- data/lib/generators/hibiki/rails/scaffold_post_install.rb +11 -0
- data/lib/generators/hibiki/rails/upload_field_injections.rb +5 -4
- data/lib/hibiki/rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b163c0d20f8fcca4839e8cfcae98316cae9b8d01bd4f81eece18e02a9ae39be
|
|
4
|
+
data.tar.gz: bf61ca3db0cd1ad769a7f5ee931a06362124841e4d62ba0bd55856d892511b85
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 317d9a267de7853075cc35f089099f5f5fc89060afa2db3afd298d8b955f5dd441fdd90d1d2780be561280c6a9e514f827901112ede534a6dd82f06cf763e9a3
|
|
7
|
+
data.tar.gz: 2623fa07782f9d5264592c4bc3e977cb229967b1810f1b05c121bd16dbbc546335c420fd855751ae0dda71e6a148b648623a010344dbec21283f764f76d9f813
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ The gem and the npm package are released in lockstep and share these version
|
|
|
4
4
|
numbers — `app/assets/javascripts/hibiki.js` is a single copy served both ways,
|
|
5
5
|
so importmap and bundler apps always resolve identical client code.
|
|
6
6
|
|
|
7
|
+
## 0.14.0 — 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
**The infinite-scroll Load-more control has a degraded path.** Under
|
|
12
|
+
`--infinite-scroll` the scaffold emitted one button that answered both a
|
|
13
|
+
click and the `visible` sentinel event, and gave it no `fallback:` on
|
|
14
|
+
purpose: a sentinel that navigates on a dead socket would make scrolling
|
|
15
|
+
navigate. Without JavaScript the button rendered and did nothing. The control
|
|
16
|
+
is now two elements. The wrapper (`books_load_more`, the same id) is the
|
|
17
|
+
sentinel and carries only `visible`. A link inside it is the click path, with
|
|
18
|
+
`fallback: true` and a real href: the query already reads `?page=N` as a
|
|
19
|
+
growing window, so a degraded click shows every window up to N, and the
|
|
20
|
+
link's fragment lands on the last row the reader had already seen. The JS
|
|
21
|
+
client is unchanged.
|
|
22
|
+
|
|
23
|
+
- Both view layers. The Phlex component uses a bare `a`, so no `LinkTo`
|
|
24
|
+
include; `page_url` is now emitted in both pagination modes.
|
|
25
|
+
- Regenerating an app changes only `_list.html.erb` / `list.rb`. A system
|
|
26
|
+
test that clicked `#books_load_more button` now clicks the `a`.
|
|
27
|
+
- Docs: the fallback contract in [The JS client] gains the rule — never give
|
|
28
|
+
a `visible` control a `fallback:`; split it as the scaffold does.
|
|
29
|
+
|
|
30
|
+
## 0.13.0 — 2026-09-07
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
**The scaffold's query object lives in `app/queries`.** `hibiki:rails:scaffold`
|
|
35
|
+
and `scaffold_controller` write `app/queries/book_query.rb` where they wrote
|
|
36
|
+
`app/models/book_query.rb`. The constant is unchanged. `app/queries` is the
|
|
37
|
+
directory the query-object gems (ARQO, querifier, query_delegator) already
|
|
38
|
+
generate into, so a query one of them writes for the same model meets ours as
|
|
39
|
+
a file conflict instead of a second `BookQuery` that Rails loads first. The
|
|
40
|
+
move costs no extra restart: the scaffold already creates `app/forms`, and one
|
|
41
|
+
restart picks up both.
|
|
42
|
+
|
|
43
|
+
- Apps scaffolded earlier need nothing. Move the file with `git mv` when
|
|
44
|
+
convenient; `hibiki:rails:multiselect` and `upload_field` find it in either
|
|
45
|
+
place.
|
|
46
|
+
- A `--force` re-run writes `app/queries`. Rails autoloads `app/models` first,
|
|
47
|
+
so the post-install output asks you to delete the old file.
|
|
48
|
+
|
|
7
49
|
## 0.12.0 — 2026-09-06
|
|
8
50
|
|
|
9
51
|
### Added
|
data/README.md
CHANGED
|
@@ -112,8 +112,8 @@ once per app, shared by every generated resource, and wired into your stylesheet
|
|
|
112
112
|
or layout automatically — the post-install output says which, or gives you the
|
|
113
113
|
line to add when it cannot tell.
|
|
114
114
|
|
|
115
|
-
Restart the server afterwards: `app/forms/`
|
|
116
|
-
autoload paths at boot.
|
|
115
|
+
Restart the server afterwards: `app/forms/` and `app/queries/` are new, and
|
|
116
|
+
Rails works out its autoload paths at boot.
|
|
117
117
|
|
|
118
118
|
### Render the reactive component
|
|
119
119
|
|
|
@@ -98,7 +98,7 @@ module Hibiki
|
|
|
98
98
|
|
|
99
99
|
# Rows are strict_loading, so the display line raises without these.
|
|
100
100
|
def inject_query_preload
|
|
101
|
-
inject_includes
|
|
101
|
+
inject_includes existing_query_path,
|
|
102
102
|
with_existing: /matched_scope\.includes\(([^)]*)\)/,
|
|
103
103
|
without: "apply_sort(matched_scope)",
|
|
104
104
|
wrapped: "apply_sort(matched_scope.includes(:#{association_name}))"
|
|
@@ -18,7 +18,7 @@ Example:
|
|
|
18
18
|
This will create:
|
|
19
19
|
db/migrate/XXXXXXXXXXXX_create_books.rb
|
|
20
20
|
app/models/book.rb
|
|
21
|
-
app/
|
|
21
|
+
app/queries/book_query.rb
|
|
22
22
|
app/channels/books_channel.rb
|
|
23
23
|
app/channels/book_channel.rb
|
|
24
24
|
app/forms/book_form.rb
|
|
@@ -28,7 +28,8 @@ Example:
|
|
|
28
28
|
And add to config/routes.rb:
|
|
29
29
|
resources :books
|
|
30
30
|
|
|
31
|
-
Restart the server afterwards if it is running — app/forms
|
|
31
|
+
Restart the server afterwards if it is running — app/forms and
|
|
32
|
+
app/queries are new.
|
|
32
33
|
|
|
33
34
|
Pass --phlex to emit Phlex components under app/views/books/*.rb instead of
|
|
34
35
|
ERB templates. The view layer is the only thing it changes; it needs the
|
|
@@ -86,7 +86,7 @@ module Hibiki
|
|
|
86
86
|
# Captured BEFORE anything is written, so post_install can tell which
|
|
87
87
|
# app/* directories are new — Rails computes autoload paths from that
|
|
88
88
|
# glob at boot, and a new one needs a restart.
|
|
89
|
-
@new_app_dirs = %w[app/forms app/channels app/models app/views].reject { exists?(it) }
|
|
89
|
+
@new_app_dirs = %w[app/forms app/queries app/channels app/models app/views].reject { exists?(it) }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# One file per app, not per resource, and the only thing this generator
|
data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/list.rb.tt
CHANGED
|
@@ -61,10 +61,11 @@ class <%= view_class_name(:list) %> < Views::Base
|
|
|
61
61
|
pages = <%= query_class_name %>.page_numbers(page: @page, page_count: @page_count)
|
|
62
62
|
<%= render_call(:pagination, ["page: @page", "page_count: @page_count", "pages: pages", %(anchor: "##{list_dom_id}"), "id: id", "url: page_url"], indent: 4, shared: true) %>
|
|
63
63
|
end
|
|
64
|
+
<% end -%>
|
|
64
65
|
|
|
65
|
-
# Real
|
|
66
|
-
# re-renders them with url_params fresh, so the
|
|
67
|
-
# truthful as the live state moves.
|
|
66
|
+
# Real hrefs for the <%= infinite? ? "Load-more link" : "page links" %>, carrying the current query
|
|
67
|
+
# state — the channel re-renders them with url_params fresh, so the
|
|
68
|
+
# fallback URLs stay truthful as the live state moves.
|
|
68
69
|
def page_url
|
|
69
70
|
->(n) do
|
|
70
71
|
query = @url_params.except(:page)
|
|
@@ -72,19 +73,22 @@ class <%= view_class_name(:list) %> < Views::Base
|
|
|
72
73
|
<%= index_helper(type: :path) %>(**query)
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
|
-
<% end -%>
|
|
76
76
|
<% if infinite? -%>
|
|
77
77
|
|
|
78
|
-
# `visible` is a pseudo-event backed by an IntersectionObserver
|
|
79
|
-
#
|
|
80
|
-
#
|
|
78
|
+
# `visible` is a pseudo-event backed by an IntersectionObserver: the wrapper
|
|
79
|
+
# is the scroll sentinel, the link inside it is the click path and its href
|
|
80
|
+
# the fallback. A sentinel must never fall back — scrolling into view on a
|
|
81
|
+
# dead socket would navigate.
|
|
81
82
|
def load_more
|
|
82
|
-
div<%= arg_list(%(id: "#{load_more_dom_id}"), *css_args(:load_more_wrap)) %> do
|
|
83
|
-
|
|
83
|
+
div<%= arg_list(%(id: "#{load_more_dom_id}"), *css_args(:load_more_wrap), "**on(:go_to_page, event: :visible, with: { page: @page + 1 })", indent: 8) %> do
|
|
84
|
+
# The STRING "false", not the boolean — Phlex omits false-valued attributes.
|
|
85
|
+
a<%= arg_list("href: load_more_href", *css_args(:btn_wide), "data: { turbo: \"false\" }.merge(on(:go_to_page, with: { page: @page + 1 },\n#{' ' * 42}fallback: true)[:data])", indent: 8) %> do
|
|
84
86
|
plain "Load more"
|
|
85
87
|
span<%= arg_list(*css_args(:muted_inline)) %> { " · #{@remaining} remaining" }
|
|
86
88
|
end
|
|
87
89
|
end
|
|
88
90
|
end
|
|
91
|
+
|
|
92
|
+
def load_more_href = "#{page_url.(@page + 1)}#<%= row_dom_id_prefix %>_#{@<%= controller_file_name %>.last.id}"
|
|
89
93
|
<% end -%>
|
|
90
94
|
end
|
data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_list.html.erb.tt
CHANGED
|
@@ -3,16 +3,14 @@
|
|
|
3
3
|
<%%# locals: (<%= list_locals_signature %>) -%>
|
|
4
4
|
<%%# The island's re-rendered fragment. Its root id is the broadcast target —
|
|
5
5
|
deliberately not the island root, which carries the stream source. %>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
truthful as the live state moves. %>
|
|
6
|
+
<%%# Real hrefs for the <%= infinite? ? "Load-more link" : "page links" %>, carrying the current query
|
|
7
|
+
state — the channel re-renders them with url_params fresh, so the
|
|
8
|
+
fallback URLs stay truthful as the live state moves. %>
|
|
10
9
|
<%% page_url = ->(n) do
|
|
11
10
|
query = url_params.except(:page)
|
|
12
11
|
query[:page] = n unless n == 1
|
|
13
12
|
<%= index_helper(type: :path) %>(**query)
|
|
14
13
|
end %>
|
|
15
|
-
<% end -%>
|
|
16
14
|
<div id="<%= list_dom_id %>"<%= css_attr(:list) %>>
|
|
17
15
|
<% if inline_create? -%>
|
|
18
16
|
<%%# The inline create form — only the channel ever renders it (the New
|
|
@@ -44,20 +42,25 @@
|
|
|
44
42
|
<%% end %>
|
|
45
43
|
|
|
46
44
|
<% if infinite? -%>
|
|
47
|
-
<%%# `visible` is a pseudo-event backed by an IntersectionObserver
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
<%%# `visible` is a pseudo-event backed by an IntersectionObserver: the wrapper
|
|
46
|
+
is the scroll sentinel, the link inside it is the click path and its href
|
|
47
|
+
the fallback. A sentinel must never fall back — scrolling into view on a
|
|
48
|
+
dead socket would navigate. %>
|
|
50
49
|
<%% if remaining.positive? %>
|
|
51
|
-
|
|
52
|
-
<%%= tag.button(type: "button",
|
|
50
|
+
<%%= tag.div(id: "<%= load_more_dom_id %>",
|
|
53
51
|
<% if css? -%>
|
|
54
|
-
|
|
52
|
+
class: "<%= css(:load_more_wrap) %>",
|
|
55
53
|
<% end -%>
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
**on(:go_to_page, event: :visible, with: { page: page + 1 })) do %>
|
|
55
|
+
<%%= link_to "#{page_url.(page + 1)}#<%= row_dom_id_prefix %>_#{<%= controller_file_name %>.last.id}",
|
|
56
|
+
<% if css? -%>
|
|
57
|
+
class: "<%= css(:btn_wide) %>",
|
|
58
|
+
<% end -%>
|
|
59
|
+
data: { turbo: false }.merge(on(:go_to_page, with: { page: page + 1 },
|
|
60
|
+
fallback: true)[:data]) do %>
|
|
58
61
|
Load more<span<%= css_attr(:muted_inline) %>> · <%%= remaining %> remaining</span>
|
|
59
62
|
<%% end %>
|
|
60
|
-
|
|
63
|
+
<%% end %>
|
|
61
64
|
<%% end %>
|
|
62
65
|
<% else -%>
|
|
63
66
|
<%%= render "shared/pagination", page: page, page_count: page_count, pages: pages,
|
|
@@ -49,19 +49,26 @@ module Hibiki
|
|
|
49
49
|
def scaffold_controller_class_name = "#{controller_class_name}Controller"
|
|
50
50
|
|
|
51
51
|
# ---- destinations ----------------------------------------------------
|
|
52
|
-
#
|
|
53
|
-
# The query object goes in app/models, NOT a new app/queries: Rails
|
|
54
|
-
# computes autoload paths from the app/* glob at boot, so a new
|
|
55
|
-
# top-level directory is not autoloadable until a server restart.
|
|
56
|
-
# app/forms already costs one restart; two would be gratuitous.
|
|
57
52
|
|
|
58
53
|
def view_dir = File.join("app/views", controller_file_path)
|
|
59
54
|
def collection_channel_path = File.join("app/channels", "#{controller_file_path}_channel.rb")
|
|
60
55
|
def member_channel_path = File.join("app/channels", *class_path, "#{file_name}_channel.rb")
|
|
61
56
|
def model_path = File.join("app/models", *class_path, "#{file_name}.rb")
|
|
62
|
-
def query_path = File.join("app/
|
|
57
|
+
def query_path = File.join("app/queries", *class_path, "#{file_name}_query.rb")
|
|
63
58
|
def form_path = File.join("app/forms", *class_path, "#{file_name}_form.rb")
|
|
64
59
|
|
|
60
|
+
# Scaffolds before 0.13.0 wrote the query object to app/models. The
|
|
61
|
+
# add-on generators read existing_query_path so their injections land
|
|
62
|
+
# in the file the app loads; the scaffold itself always writes query_path.
|
|
63
|
+
def legacy_query_path = File.join("app/models", *class_path, "#{file_name}_query.rb")
|
|
64
|
+
|
|
65
|
+
def existing_query_path
|
|
66
|
+
return query_path if File.exist?(File.join(destination_root, query_path))
|
|
67
|
+
return legacy_query_path if File.exist?(File.join(destination_root, legacy_query_path))
|
|
68
|
+
|
|
69
|
+
query_path
|
|
70
|
+
end
|
|
71
|
+
|
|
65
72
|
def scaffold_controller_path
|
|
66
73
|
File.join("app/controllers", "#{controller_file_path}_controller.rb")
|
|
67
74
|
end
|
|
@@ -23,6 +23,7 @@ module Hibiki
|
|
|
23
23
|
# stylesheet, a leftover file. None of them is about the resource.
|
|
24
24
|
def app_notices
|
|
25
25
|
restart_notice
|
|
26
|
+
stale_query_notice
|
|
26
27
|
# These live with the code that chose their branch, like
|
|
27
28
|
# parent_notices — the outcomes are those modules' vocabulary.
|
|
28
29
|
stylesheet_notice
|
|
@@ -49,6 +50,16 @@ module Hibiki
|
|
|
49
50
|
"Please restart if the server is running.", :yellow
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
# Scaffolds before 0.13.0 wrote the query object to app/models, which
|
|
54
|
+
# autoloads ahead of app/queries: until the old copy goes, it is the one
|
|
55
|
+
# the app loads. A generator never deletes.
|
|
56
|
+
def stale_query_notice
|
|
57
|
+
return unless exists?(legacy_query_path)
|
|
58
|
+
|
|
59
|
+
say_status :stale, "#{legacy_query_path} has moved to #{query_path}. " \
|
|
60
|
+
"Please delete the old file.", :yellow
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
def rebuild_css_notice
|
|
53
64
|
return unless css?
|
|
54
65
|
|
|
@@ -103,13 +103,14 @@ module Hibiki
|
|
|
103
103
|
# Rows are strict_loading and frozen, so the thumbnail raises without
|
|
104
104
|
# these — on the index AND on show-page repaints (the member channel).
|
|
105
105
|
def inject_query_preload
|
|
106
|
-
|
|
106
|
+
path = existing_query_path
|
|
107
|
+
return if wired?(path, /\.#{with_attached}\b/)
|
|
107
108
|
|
|
108
|
-
unless wired?(
|
|
109
|
-
return manual_wiring(
|
|
109
|
+
unless wired?(path, QUERY_WINDOW_SCOPE)
|
|
110
|
+
return manual_wiring(path, " # preload the attachment:\n .#{with_attached}")
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
gsub_file(
|
|
113
|
+
gsub_file(path, QUERY_WINDOW_SCOPE) do |match|
|
|
113
114
|
head, scope, tail = match.match(QUERY_WINDOW_SCOPE).captures
|
|
114
115
|
"#{head}#{scope}.#{with_attached}#{tail}"
|
|
115
116
|
end
|
data/lib/hibiki/rails/version.rb
CHANGED