concerns_on_rails 1.28.0 → 1.28.1
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 +32 -0
- data/README.md +39 -4
- data/lib/concerns_on_rails/controllers/paginatable.rb +52 -5
- data/lib/concerns_on_rails/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00337457f7ea35fd2e02bb74bcb4031208109536946b448706104a9e031bd9ea
|
|
4
|
+
data.tar.gz: 3bfc8749303d212e51d98d8dd7fa4323888e3d984444c768695d5c290df311a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50ea50c59f6b15978fe3f1cc86f03266998249bd4b17dc84b36ff6a717e9a57d0efba37f2e5648c58009761aeb2198f14368b2c20715c0bc40c246c1e7b5ee25
|
|
7
|
+
data.tar.gz: f6b8c7784e26ac3f5c8f14217e07cbe6d846ecab216fad6c61fb192bf06a18aa4484d80efb32fd82d625c683d917d9fb8c9b88724adcf2a11878fb0ea6a1f7e9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
<!-- CHANGELOG.md -->
|
|
2
2
|
|
|
3
|
+
## 1.28.1 (2026-09-07)
|
|
4
|
+
|
|
5
|
+
One additive Paginatable option (#89): `pagination_meta` can now publish the
|
|
6
|
+
page window a pagination bar needs — first, last and N pages either side of the
|
|
7
|
+
current page — instead of leaving every client to compute it from
|
|
8
|
+
`total_pages`. Opt-in, controller-only, no extra query, and existing responses
|
|
9
|
+
are byte-for-byte unchanged. 1314 examples, 0 failures.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Controllers::Paginatable**: `paginate_by window: 3` adds a `pages:` key to
|
|
13
|
+
`pagination_meta` — the first page, the last page, and N pages either side of
|
|
14
|
+
the current one, with the Symbol `:gap` standing in for each run left out
|
|
15
|
+
(`[1, :gap, 44, 45, 46, 47, 48, 49, 50, :gap, 100]`) — enough to render a
|
|
16
|
+
`1 … 44 45 46 [47] 48 49 50 … 100` bar straight from the meta Hash. Opt-in:
|
|
17
|
+
without `window:` the key is absent entirely (not `nil`), so no existing
|
|
18
|
+
meta Hash or serialized body changes shape, and `meta.key?(:pages)` is a
|
|
19
|
+
clean probe. The window is arithmetic over the `total` already counted, so it
|
|
20
|
+
costs no extra query on either the memoized `paginated` path or the fresh
|
|
21
|
+
`pagination_meta` one. A jump of exactly two pages is filled with the page it
|
|
22
|
+
would have hidden (`1 2 3`, never the wider `1 … 3`), and a `?page=` past the
|
|
23
|
+
last page windows around the last page as `rel="prev"` already does.
|
|
24
|
+
`window:` is validated at declaration — a non-negative Integer, or
|
|
25
|
+
`nil`/`false` to disable; `0` yields first, current and last only. Headers
|
|
26
|
+
and `Link` rels are unchanged: `pages:` is body-only by design. (#89)
|
|
27
|
+
|
|
28
|
+
### Internal
|
|
29
|
+
- `set_pagination_headers` no longer splats the memoized meta Hash, which
|
|
30
|
+
raised `unknown keyword: :pages` once that Hash could carry the window; the
|
|
31
|
+
four header values are passed explicitly.
|
|
32
|
+
- README's advertised example counts were stale (1,160 and 1,303) and now read
|
|
33
|
+
1,314.
|
|
34
|
+
|
|
3
35
|
## 1.28.0 (2026-09-06)
|
|
4
36
|
|
|
5
37
|
Pagination is the theme: the four-PR Paginatable stack (#40, #45, #62, #83)
|
data/README.md
CHANGED
|
@@ -148,7 +148,7 @@ across all 43 concerns — press <kbd>/</kbd> and type.
|
|
|
148
148
|
- **Lean dependencies** — only `acts_as_list` (Sortable) and `friendly_id` (Sluggable), and both load **lazily**: an app that never includes those concerns never loads them. Depends on `activerecord`/`actionpack`/`activesupport`, not the full `rails` meta-gem; controller concerns have zero extra deps
|
|
149
149
|
- **Schema-validated configuration** — every macro checks that the configured column exists and raises `ArgumentError` early — with a ready-to-paste `rails generate migration` hint when it doesn't
|
|
150
150
|
- **Composable** — concerns are independent; mix and match per model
|
|
151
|
-
- **Tested like an app, not a snippet** — **1,
|
|
151
|
+
- **Tested like an app, not a snippet** — **1,314 RSpec examples** run against a real database on every CI build
|
|
152
152
|
- **Documented twice** — everything in this README also lives as a per-concern page on the [docs site](https://vsn2015.github.io/concerns_on_rails), searchable and deep-linkable
|
|
153
153
|
|
|
154
154
|
---
|
|
@@ -158,7 +158,7 @@ across all 43 concerns — press <kbd>/</kbd> and type.
|
|
|
158
158
|
Add to your application's `Gemfile`:
|
|
159
159
|
|
|
160
160
|
```ruby
|
|
161
|
-
gem "concerns_on_rails", "~> 1.
|
|
161
|
+
gem "concerns_on_rails", "~> 1.28"
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
Or pull the latest from GitHub:
|
|
@@ -1494,6 +1494,41 @@ paginate_by style: :jsonapi # ?page[numbe
|
|
|
1494
1494
|
paginate_by page_param: %i[paging page], per_page_param: %i[paging per] # any nested path
|
|
1495
1495
|
```
|
|
1496
1496
|
|
|
1497
|
+
**Page window for a pagination bar**
|
|
1498
|
+
|
|
1499
|
+
`window:` adds a `pages:` key to `pagination_meta` — the first page, the last page, and N pages
|
|
1500
|
+
either side of the current one, with `:gap` standing in for the runs left out. It is opt-in:
|
|
1501
|
+
without `window:` the key is absent entirely. No extra query either way — it is arithmetic over
|
|
1502
|
+
the total already counted.
|
|
1503
|
+
|
|
1504
|
+
```ruby
|
|
1505
|
+
paginate_by per_page: 10, window: 3
|
|
1506
|
+
|
|
1507
|
+
pagination_meta
|
|
1508
|
+
# => { total: 1000, page: 47, per_page: 10, total_pages: 100,
|
|
1509
|
+
# pages: [1, :gap, 44, 45, 46, 47, 48, 49, 50, :gap, 100] }
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
Render it straight into a `1 … 44 45 46 [47] 48 49 50 … 100` bar:
|
|
1513
|
+
|
|
1514
|
+
```erb
|
|
1515
|
+
<% pagination_meta[:pages].each do |page| %>
|
|
1516
|
+
<%= page == :gap ? "…" : link_to(page, url_for(page: page)) %>
|
|
1517
|
+
<% end %>
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
| Situation | `pages:` |
|
|
1521
|
+
|-----------|----------|
|
|
1522
|
+
| `?page=47` of 100 | `[1, :gap, 44, 45, 46, 47, 48, 49, 50, :gap, 100]` |
|
|
1523
|
+
| `?page=2` of 100 | `[1, 2, 3, 4, 5, :gap, 100]` — no leading gap once the window reaches page 1 |
|
|
1524
|
+
| `?page=6` of 100 | `[1, 2, 3, 4, 5, 6, 7, 8, 9, :gap, 100]` — a one-page gap is filled, never `1 … 3` |
|
|
1525
|
+
| 5 pages total | `[1, 2, 3, 4, 5]` — the window spans everything |
|
|
1526
|
+
| empty collection | key absent |
|
|
1527
|
+
| `window: 0` | `[1, :gap, 47, :gap, 100]` — first, current and last only |
|
|
1528
|
+
|
|
1529
|
+
`?page=` past the last page windows around the last page (as `rel="prev"` already does), and
|
|
1530
|
+
`window:` must be a non-negative Integer or `nil`/`false` — validated at declaration.
|
|
1531
|
+
|
|
1497
1532
|
**Response headers**: `X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`, and an RFC 8288 `Link`
|
|
1498
1533
|
header with `first` / `prev` / `next` / `last` URLs rebuilt from the current request (other query params
|
|
1499
1534
|
preserved; `prev`/`next` only when such a page exists; nothing for an empty collection) — the GitHub
|
|
@@ -2122,9 +2157,9 @@ Point your agent at `llms.txt` for an overview, or paste a single concern's `.md
|
|
|
2122
2157
|
|
|
2123
2158
|
```sh
|
|
2124
2159
|
bundle install # install dev dependencies
|
|
2125
|
-
bundle exec rspec # run the test suite (1,
|
|
2160
|
+
bundle exec rspec # run the test suite (1,314 examples)
|
|
2126
2161
|
gem build concerns_on_rails.gemspec # build the gem
|
|
2127
|
-
gem install ./concerns_on_rails-1.28.
|
|
2162
|
+
gem install ./concerns_on_rails-1.28.1.gem # install locally
|
|
2128
2163
|
|
|
2129
2164
|
# Preview the docs site locally (GitHub Pages serves docs/ as-is):
|
|
2130
2165
|
cd docs && python3 -m http.server 8000 # → http://localhost:8000
|
|
@@ -41,6 +41,9 @@ module ConcernsOnRails
|
|
|
41
41
|
class_attribute :paginatable_per_page, default: DEFAULT_PER_PAGE
|
|
42
42
|
class_attribute :paginatable_max_per_page, default: DEFAULT_MAX_PER_PAGE
|
|
43
43
|
class_attribute :paginatable_link_header, default: true
|
|
44
|
+
# Half-width of the page window in `pagination_meta[:pages]`; nil = no
|
|
45
|
+
# window, and no `pages:` key at all.
|
|
46
|
+
class_attribute :paginatable_window, default: nil
|
|
44
47
|
# Where page / per_page are read from — a path of param names (`["page"]`,
|
|
45
48
|
# or `["page", "number"]` for JSON:API's page[number]).
|
|
46
49
|
class_attribute :paginatable_page_param, default: %w[page].freeze
|
|
@@ -55,10 +58,11 @@ module ConcernsOnRails
|
|
|
55
58
|
# Example:
|
|
56
59
|
# paginate_by per_page: 50, max_per_page: 500, link_header: false
|
|
57
60
|
def paginate_by(per_page: DEFAULT_PER_PAGE, max_per_page: DEFAULT_MAX_PER_PAGE, link_header: true,
|
|
58
|
-
page_param: nil, per_page_param: nil, style: :flat)
|
|
61
|
+
page_param: nil, per_page_param: nil, style: :flat, window: nil)
|
|
59
62
|
self.paginatable_per_page = per_page.to_i
|
|
60
63
|
self.paginatable_max_per_page = max_per_page.to_i
|
|
61
64
|
self.paginatable_link_header = link_header ? true : false
|
|
65
|
+
self.paginatable_window = paginatable_window!(window)
|
|
62
66
|
defaults = paginatable_style_params!(style)
|
|
63
67
|
self.paginatable_page_param = paginatable_param_path!(:page_param, page_param || defaults[0])
|
|
64
68
|
self.paginatable_per_page_param = paginatable_param_path!(:per_page_param, per_page_param || defaults[1])
|
|
@@ -75,6 +79,15 @@ module ConcernsOnRails
|
|
|
75
79
|
end
|
|
76
80
|
end
|
|
77
81
|
|
|
82
|
+
# nil / false disable the window (no `pages:` key). `0` is meaningful:
|
|
83
|
+
# first, current and last only.
|
|
84
|
+
def paginatable_window!(value)
|
|
85
|
+
return nil if value.nil? || value == false
|
|
86
|
+
return value if value.is_a?(Integer) && !value.negative?
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, "#{LABEL}: window: must be a non-negative Integer or nil (got #{value.inspect})"
|
|
89
|
+
end
|
|
90
|
+
|
|
78
91
|
# A name or a non-empty path of names, normalized to Strings.
|
|
79
92
|
def paginatable_param_path!(option, value)
|
|
80
93
|
path = Array(value)
|
|
@@ -117,8 +130,9 @@ module ConcernsOnRails
|
|
|
117
130
|
source.limit(per_page).offset(offset)
|
|
118
131
|
end
|
|
119
132
|
|
|
120
|
-
@paginatable_meta =
|
|
121
|
-
|
|
133
|
+
@paginatable_meta =
|
|
134
|
+
paginatable_windowed(total: total, page: page, per_page: per_page, total_pages: total_pages)
|
|
135
|
+
set_pagination_headers(total: total, page: page, per_page: per_page, total_pages: total_pages)
|
|
122
136
|
set_pagination_links(page: page, total_pages: total_pages)
|
|
123
137
|
records
|
|
124
138
|
end
|
|
@@ -134,12 +148,12 @@ module ConcernsOnRails
|
|
|
134
148
|
|
|
135
149
|
total = paginatable_meta_total(collection, total)
|
|
136
150
|
per_page = pagination_per_page
|
|
137
|
-
|
|
151
|
+
paginatable_windowed(
|
|
138
152
|
total: total,
|
|
139
153
|
page: pagination_page,
|
|
140
154
|
per_page: per_page,
|
|
141
155
|
total_pages: per_page.positive? ? (total.to_f / per_page).ceil : 0
|
|
142
|
-
|
|
156
|
+
)
|
|
143
157
|
end
|
|
144
158
|
|
|
145
159
|
private
|
|
@@ -228,6 +242,39 @@ module ConcernsOnRails
|
|
|
228
242
|
{ path.first.to_sym => nested }
|
|
229
243
|
end
|
|
230
244
|
|
|
245
|
+
# Adds `pages:` to a meta Hash when `window:` is declared, and leaves the
|
|
246
|
+
# Hash untouched otherwise — the key is absent, never nil, so
|
|
247
|
+
# `meta.key?(:pages)` is a clean opt-in probe.
|
|
248
|
+
def paginatable_windowed(meta)
|
|
249
|
+
pages = paginatable_page_window(meta[:page], meta[:total_pages])
|
|
250
|
+
pages ? meta.merge(pages: pages) : meta
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# First, last, and `window` pages either side of the current page, with
|
|
254
|
+
# :gap standing in for the runs left out — [1, :gap, 46, 47, 48, :gap, 100].
|
|
255
|
+
# Pure arithmetic over the total already counted: no extra query. A page
|
|
256
|
+
# past the last one windows around the last page, as `prev` already does.
|
|
257
|
+
def paginatable_page_window(page, total_pages)
|
|
258
|
+
window = self.class.paginatable_window
|
|
259
|
+
return nil if window.nil? || total_pages < 1
|
|
260
|
+
|
|
261
|
+
current = page.clamp(1, total_pages)
|
|
262
|
+
from = [current - window, 1].max
|
|
263
|
+
to = [current + window, total_pages].min
|
|
264
|
+
paginatable_insert_gaps(([1, total_pages] + (from..to).to_a).uniq.sort)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# A jump of exactly two pages is filled with the page it would have
|
|
268
|
+
# hidden — "1 2 3", never the wider "1 … 3"; anything longer collapses
|
|
269
|
+
# into one :gap.
|
|
270
|
+
def paginatable_insert_gaps(numbers)
|
|
271
|
+
numbers.each_cons(2).with_object([numbers.first]) do |(previous, current), result|
|
|
272
|
+
result << (previous + 1) if current - previous == 2
|
|
273
|
+
result << :gap if current - previous > 2
|
|
274
|
+
result << current
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
231
278
|
def set_pagination_headers(total:, page:, per_page:, total_pages:)
|
|
232
279
|
return unless respond_to?(:response) && response
|
|
233
280
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: concerns_on_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.28.
|
|
4
|
+
version: 1.28.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ethan Nguyen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionpack
|