layered-resource-rails 0.3.0 → 0.3.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/.claude/skills/layered-resource-rails/SKILL.md +1 -1
- data/CHANGELOG.md +6 -0
- data/README.md +1 -1
- data/app/helpers/layered/resource/filters_helper.rb +11 -3
- data/lib/layered/resource/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: 8bc9f9c8cd7248c93dcfe75e5af6c62e73f15e12f7a6a2092b213bbf3c5ffaf3
|
|
4
|
+
data.tar.gz: bc8992afbc303af01d63f9a0d86e01e3141e5cff9b47b4f6d61db7b7d68c560a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91a5a65b3a8b3ac9108046f040e9b38d5ad58e79cd457343cea5d25cedf8c37a16d4077fcaf7d12f5a7da0ecbfa07e293c88169342048f82bc508df47d1606f1
|
|
7
|
+
data.tar.gz: eaf791793476475fc62b310fe841c186fb23561302aad6e26a06fe0f6f4c975de37e32f1fdc4360af9b3f1eb1ad41a48a34587af9939e5a72603ebed08246ede
|
|
@@ -177,7 +177,7 @@ Inference by column type: `enum`/`belongs_to` -> multi-select (`_in`; `multiple:
|
|
|
177
177
|
|
|
178
178
|
**Long option lists switch control.** A select-type filter renders as the plain list (multi-select: checkboxes + Apply; single-choice: instant-apply links) up to `Layered::Resource.filter_combobox_threshold` options (10), and as a type-ahead **combobox** past it — a checkbox list of every user is no way to pick one. A single-choice combobox posts a scalar (no `[]`), so `_eq` gets the value rather than an array. The count is taken per request, after a `collection:` callable resolves, so the control follows the data; the decision lives in `layered_filter_control` (the view helper), not in `resolved_filters`, for exactly that reason. Declaring `as:` pins the control and opts out: `as: :select` keeps the list however long, `as: :combobox` uses the combobox however short.
|
|
179
179
|
|
|
180
|
-
**Remote options.** `url:` fetches the options from an endpoint as the user types instead of rendering a collection — such a filter is always a combobox (nothing to render or count). Give it as a callable (`-> { user_options_path }`) so it resolves per request in the view, where route helpers exist. The endpoint is an ordinary host-app action including `Layered::Ui::ComboboxOptions` and returning `l_ui_combobox_options(scope, label:, search:)` — the gem never routes it, so it's authorised however any index is. Since the browser has no collection to look a label up in, an active remote filter's values are labelled server-side from the records (same `name`/`title`/`label`/`email` fallbacks), so the tag reads "User: Alice" not "User: 12"; a `url:` over a plain column has no records and its values label themselves. `min_chars:` and `text:` pass through too; the write-side options (`create:`, `create_name:`, `reorder:`) deliberately don't — a filter picks among values that already exist.
|
|
180
|
+
**Remote options.** `url:` fetches the options from an endpoint as the user types instead of rendering a collection — such a filter is always a combobox (nothing to render or count). Give it as a callable (`-> { user_options_path }`) so it resolves per request in the view, where route helpers exist. The endpoint is an ordinary host-app action including `Layered::Ui::ComboboxOptions` and returning `l_ui_combobox_options(scope, label:, search:)` — the gem never routes it, so it's authorised however any index is. Since the browser has no collection to look a label up in, an active remote filter's values are labelled server-side from the records (same `name`/`title`/`label`/`email` fallbacks), so the tag reads "User: Alice" not "User: 12"; a `url:` over a plain column has no records and its values label themselves. The label lookup is confined to records the resource's `scope(controller)` references (via the foreign key), so a hand-edited id outside it - e.g. another tenant's user - labels itself instead of leaking a name; the cost is that an in-scope option with no matching records also shows its raw id. `min_chars:` and `text:` pass through too; the write-side options (`create:`, `create_name:`, `reorder:`) deliberately don't — a filter picks among values that already exist.
|
|
181
181
|
|
|
182
182
|
**The blank a combobox posts.** `l_ui_combobox` emits a leading blank hidden input so clearing every token submits an empty collection rather than omitting the key. Ransack prunes blanks from an `_in` array itself, and `layered_filter_query_params` prunes them too — an array left with nothing becomes an explicit `""`, the same shape the Clear link writes, so the tag reads as inactive *and* a `default:` doesn't immediately re-apply.
|
|
183
183
|
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. This project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [0.3.1] - 2026-10-04
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
|
|
9
|
+
- A remote (`url:`) `belongs_to` filter no longer labels records outside the resource's `scope`. Its current values come from the query string, and the labels for the tag and combobox tokens were looked up with an unscoped `klass.where(id: ...)`, so hand-editing the id (`?q[user_id_in][]=`) revealed the name or e-mail of any record, including another tenant's. The lookup now only reads records the resource's `scope(controller)` references through the foreign key. Any other value shows its raw id instead.
|
|
10
|
+
|
|
5
11
|
## [0.3.0] - 2026-09-21
|
|
6
12
|
|
|
7
13
|
### Changed
|
data/README.md
CHANGED
|
@@ -564,7 +564,7 @@ end
|
|
|
564
564
|
|
|
565
565
|
A remote combobox has no collection in the browser to look a label up in, so an active filter's current values are labelled server-side from the records themselves (the same `name`/`title`/`label`/`email` fallbacks) — the tag reads "User: Alice", not "User: 12". A `url:` on a plain column has no records to read, so its values label themselves.
|
|
566
566
|
|
|
567
|
-
|
|
567
|
+
Current values arrive in the query string, so they can be hand-edited to any id — and the gem can't see whatever scoping the `url:` endpoint applies. Labels are therefore only read for records the resource's own `scope` references (users with a post the viewer can see, say); any other value labels itself. A guessed `?q[user_id_in][]=` for another tenant's user shows "User: 42", not their name. The flip side is that a value from the endpoint with no matching records in scope also shows its id, next to an empty table. (A `belongs_to` filter *without* `url:` renders `klass.all` as its options — pass a scoped `collection:` if those labels are private.)
|
|
568
568
|
|
|
569
569
|
`min_chars:` and `text:` pass through to `l_ui_combobox` alongside `url:`. The write-side combobox options (`create:`, `create_name:`, `reorder:`) don't: a filter picks among values that already exist.
|
|
570
570
|
|
|
@@ -286,15 +286,23 @@ module Layered
|
|
|
286
286
|
|
|
287
287
|
# Value => label for the filter's currently-set values. A remote filter
|
|
288
288
|
# has no collection to read them from, so an association's are looked up
|
|
289
|
-
# by
|
|
289
|
+
# by key; anything else (a remote filter over a plain column) resolves to
|
|
290
290
|
# nothing and the value stands in for its own label.
|
|
291
|
+
#
|
|
292
|
+
# The values come straight from the query string, and the gem can't see
|
|
293
|
+
# whatever scoping the `url:` endpoint applies, so the lookup is confined
|
|
294
|
+
# to records the resource's own `scope` references: a hand-edited id for,
|
|
295
|
+
# say, another tenant's user labels itself rather than disclosing that
|
|
296
|
+
# user's name.
|
|
291
297
|
def layered_filter_option_labels(filter, values)
|
|
292
298
|
if filter[:url].present?
|
|
293
299
|
reflection = filter[:reflection]
|
|
294
300
|
return {} if reflection.nil?
|
|
295
301
|
|
|
296
|
-
reflection.
|
|
297
|
-
|
|
302
|
+
primary_key = reflection.association_primary_key
|
|
303
|
+
referenced = @resource.scope(controller).unscope(:order).select(reflection.foreign_key)
|
|
304
|
+
reflection.klass.where(primary_key => values).where(primary_key => referenced)
|
|
305
|
+
.to_h { |record| [record.public_send(primary_key).to_s, layered_filter_record_label(record)] }
|
|
298
306
|
else
|
|
299
307
|
layered_filter_collection(filter).to_h { |label, value| [value, label] }
|
|
300
308
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: layered-resource-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -288,7 +288,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
288
288
|
- !ruby/object:Gem::Version
|
|
289
289
|
version: '0'
|
|
290
290
|
requirements: []
|
|
291
|
-
rubygems_version:
|
|
291
|
+
rubygems_version: 3.6.9
|
|
292
292
|
specification_version: 4
|
|
293
293
|
summary: Convention-over-configuration CRUD for Rails 8+ with Ransack and Pagy built
|
|
294
294
|
in.
|