crud_components 0.2.0 → 0.2.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 +10 -0
- data/README.md +2 -2
- data/docs/fields.md +6 -6
- data/docs/filtering.md +6 -5
- data/lib/crud_components/builder.rb +3 -3
- data/lib/crud_components/fields/belongs_to_field.rb +4 -5
- data/lib/crud_components/fields/has_many_field.rb +2 -2
- data/lib/crud_components/helpers.rb +1 -1
- data/lib/crud_components/like_spec.rb +3 -3
- data/lib/crud_components/structure.rb +7 -0
- data/lib/crud_components/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: 3039812d78a4d8677f871959497a18eb9f2236627a84a0d046b72a520c0ba34c
|
|
4
|
+
data.tar.gz: 70b36787f0e28e2f51af0cfdf3361dc860fcdfb048d90365ddf4ebf128759372
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7af65ee9f9aea26c3e6fc132fbe29ad3fbf2a3c8e931e209ee72dd7cb2a57cab670875a3c14fad3907ea316138a58d272729bfd6239d68be9cacf360b4e94fa9
|
|
7
|
+
data.tar.gz: 7a220eec077f9845b682c3a081e971d2a488a7d09a7bacfb6f514023fff92a3340000420d5bca17b648dfd424522c1f32a31f505eb81c2eb3331c2718fddbf87
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ This project follows [semantic versioning](https://semver.org).
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## v0.2.1 — 2026-08-01
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Verified against **Rails 8.1** — the CI matrix now covers it alongside 7.1, 7.2 and 8.0. The gem's runtime API and its declared dependencies are unchanged; the rest is tooling (GitHub Actions bumped, demo image on Ruby 4.0, CI installs libvips so the playground's Active Storage previews load).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- An association column whose target labels itself with a *method* rather than a column (e.g. `label :display_title`) no longer raises `DefinitionError` ("… is neither a column nor an association of …") when its filter or `?q=` search runs. The label-based text match — and the `belongs_to` sort — are skipped whenever there is no column behind the label, as they already were for a block label. A `belongs_to` still filters by value; spell the columns out (`filter publisher: :name`) to match such a target as text.
|
|
17
|
+
|
|
8
18
|
## v0.2.0 — 2026-07-14
|
|
9
19
|
|
|
10
20
|
### Added
|
data/README.md
CHANGED
|
@@ -216,8 +216,8 @@ search_in :title, :subtitle, :publisher # :publisher matches Publisher's label
|
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
`label` + `identify_by` + `search_in` are a model's **identity** — and they define how
|
|
219
|
-
*other* models render, link and filter it through their associations. Declare
|
|
220
|
-
identity once and every `belongs_to :
|
|
219
|
+
*other* models render, link and filter it through their associations. Declare Publisher's
|
|
220
|
+
identity once and every `belongs_to :publisher` column gets it for free. →
|
|
221
221
|
[Fields → identity](docs/fields.md#identity-label-identify_by-search_in)
|
|
222
222
|
|
|
223
223
|
### Buttons / Actions
|
data/docs/fields.md
CHANGED
|
@@ -29,7 +29,7 @@ attribute :token, filter: false # opt a derived field out
|
|
|
29
29
|
`attributes` (plural) applies shared options to several fields at once:
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
|
-
attributes :
|
|
32
|
+
attributes :purchase_price, :shop_margin, if: :manage
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
The field universe is always *all* derived columns/associations plus declared computed
|
|
@@ -268,9 +268,9 @@ These are not specific to `DynamicColumn` — a declared `attribute :status, hea
|
|
|
268
268
|
takes the same options. Everything works in the non-grouped and grouped (`group_by:`) layouts,
|
|
269
269
|
and plays with the column picker (a hidden column simply renders no header). The
|
|
270
270
|
`/column_headers` page in `test/dummy` is a full worked example. This is what lets a
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
books × properties **matrix** live entirely in `crud_collection` — one `DynamicColumn` per
|
|
272
|
+
property, its controls in its own header — instead of a hand-built controls strip above
|
|
273
|
+
the table.
|
|
274
274
|
|
|
275
275
|
## Path columns
|
|
276
276
|
|
|
@@ -332,7 +332,7 @@ icon 'book' # default: guessed from the model name (config.model_i
|
|
|
332
332
|
Block form: `label { |book| "#{book.title} (#{book.published_on&.year})" }`. With no
|
|
333
333
|
string column at all it falls back to `"Book #42"` (`model_name.human` + ` #` + id).
|
|
334
334
|
When the label reaches into associations, declare them with `preload:` so they're
|
|
335
|
-
eager-loaded wherever this model is shown — `label :
|
|
335
|
+
eager-loaded wherever this model is shown — `label :display_title, preload: %i[publisher]`
|
|
336
336
|
([Performance](performance.md#eager-loading-render-dependencies)).
|
|
337
337
|
- **`identify_by`** — the column URL params use to identify a record of this model. With
|
|
338
338
|
`identify_by :slug`, a filter URL reads `?publisher=tor-books` and resolves via
|
|
@@ -402,7 +402,7 @@ with `preload: %i[publisher]` — [Performance](performance.md#eager-loading-ren
|
|
|
402
402
|
| json | `<pre>` (rouge if present) | — | — | not form-editable in v1 |
|
|
403
403
|
| Active Storage attachment | image / preview / icon by content type | — | — | form shows current; keep/add/remove via signed_ids |
|
|
404
404
|
| `belongs_to` | nil-safe link via target `label` | select (≤ `select_limit`) / text over target `label` | v2 | resolves by `identify_by` |
|
|
405
|
-
| `has_many` / habtm | "a, b +n more" links | text over the children's `label` (
|
|
405
|
+
| `has_many` / habtm | "a, b +n more" links | text over the children's `label` (columnless label: facet) | no | "+n more" links to nested/filtered index |
|
|
406
406
|
| public method | by value type | — | — | needs a facet to filter/sort |
|
|
407
407
|
| `render` block | block output | — | — | facets add filter/sort |
|
|
408
408
|
|
data/docs/filtering.md
CHANGED
|
@@ -15,9 +15,9 @@ see") see [security.md](security.md).
|
|
|
15
15
|
Every column is filterable by default through the control its type implies — a string by
|
|
16
16
|
substring, a number/date by range, an enum/boolean by select, an association by its target's
|
|
17
17
|
**label** (the name shown in the cell): a `belongs_to` as a select/text, a `has_many`/habtm as
|
|
18
|
-
text matching any child. A *computed* column, or an association whose label
|
|
19
|
-
|
|
20
|
-
a block):
|
|
18
|
+
text matching any child. A *computed* column, or an association whose label has no column
|
|
19
|
+
behind it — a block, or a method name like `label :display_title` — opts in with the `filter`
|
|
20
|
+
facet (a [search spec](#the-search-spec) or a block):
|
|
21
21
|
|
|
22
22
|
```ruby
|
|
23
23
|
attribute :author_names do
|
|
@@ -95,8 +95,9 @@ filter :title, { authors: :name } # mixed
|
|
|
95
95
|
The **label form** — an association name *without* columns — matches the target's
|
|
96
96
|
**label**: the name shown in that association's cell ("search what you see"). It is the
|
|
97
97
|
idiomatic style and never reaches the target's other columns, so a secret column on the
|
|
98
|
-
target can't be probed through an association. When the target's label
|
|
99
|
-
|
|
98
|
+
target can't be probed through an association. When the target's label has no column behind
|
|
99
|
+
it — a custom block, or a method name like `label :display_title` — spell the columns out
|
|
100
|
+
(`filter authors: :name`).
|
|
100
101
|
|
|
101
102
|
The gem turns a spec into `left_joins` plus parameterized, wildcard-escaped `ILIKE`
|
|
102
103
|
(via `sanitize_sql_like` with an explicit `\` escape char, so `%`, `_` and `\` are all
|
|
@@ -34,7 +34,7 @@ module CrudComponents
|
|
|
34
34
|
# How a record is titled (links, headings). Give a method name or a block.
|
|
35
35
|
# @param method [Symbol, nil] a method on the record returning its label.
|
|
36
36
|
# @param preload [Array<Symbol>, Symbol, nil] associations the label reaches
|
|
37
|
-
# into (`label :
|
|
37
|
+
# into (`label :display_title, preload: %i[publisher authors]`). They're
|
|
38
38
|
# eager-loaded automatically whenever this model is shown as another
|
|
39
39
|
# model's association column — declare once, no N+1 anywhere.
|
|
40
40
|
# @yield [record] computes the label; receives the record.
|
|
@@ -51,7 +51,7 @@ module CrudComponents
|
|
|
51
51
|
# Associations to eager-load whenever this model is rendered (as a row or as
|
|
52
52
|
# another model's association cell) — for label/render dependencies the gem
|
|
53
53
|
# can't infer. Additive with `label …, preload:`; declare more than once to
|
|
54
|
-
# accumulate. e.g. `preload :
|
|
54
|
+
# accumulate. e.g. `preload :publisher, :authors`.
|
|
55
55
|
# @param names [Array<Symbol>] association names (nested hashes allowed).
|
|
56
56
|
# @return [void]
|
|
57
57
|
def preload(*names)
|
|
@@ -154,7 +154,7 @@ module CrudComponents
|
|
|
154
154
|
private
|
|
155
155
|
|
|
156
156
|
# Normalize a preload value to an array of includes-specs, leaving a nested
|
|
157
|
-
# hash (`{
|
|
157
|
+
# hash (`{ publisher: :books }`) intact (Array() would split it).
|
|
158
158
|
def preload_list(value)
|
|
159
159
|
case value
|
|
160
160
|
when nil then []
|
|
@@ -27,7 +27,7 @@ module CrudComponents
|
|
|
27
27
|
# Default ?q= reaches the target's label (the name shown in the cell).
|
|
28
28
|
# Skipped for polymorphic (no single target) or a block/columnless label.
|
|
29
29
|
def search_spec_entry
|
|
30
|
-
name if !reflection.polymorphic? && target_structure.
|
|
30
|
+
name if !reflection.polymorphic? && target_structure.label_column_name
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def derived_filterable?
|
|
@@ -70,7 +70,7 @@ module CrudComponents
|
|
|
70
70
|
|
|
71
71
|
# Load the association, nesting the target's identity_preloads (its label's
|
|
72
72
|
# own association deps) plus any per-column `preload:` so the target's
|
|
73
|
-
# label never N+1s. e.g. {
|
|
73
|
+
# label never N+1s. e.g. { book: %i[publisher authors] }.
|
|
74
74
|
# A polymorphic belongs_to has no single target class, so we can't nest its
|
|
75
75
|
# label's preloads — just preload the association itself (Rails groups it by
|
|
76
76
|
# type); the cell still renders each record's label and links it at runtime.
|
|
@@ -102,14 +102,13 @@ module CrudComponents
|
|
|
102
102
|
def sort_column
|
|
103
103
|
return nil if reflection.polymorphic?
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
col if col && target.column_names.include?(col.to_s)
|
|
105
|
+
target_structure.label_column_name
|
|
107
106
|
end
|
|
108
107
|
|
|
109
108
|
# Free text matches the target's label only — the name shown in the cell.
|
|
110
109
|
# A block/computed label has no column to match, so there's no text filter.
|
|
111
110
|
def like_subquery(scope, value)
|
|
112
|
-
label = target_structure.
|
|
111
|
+
label = target_structure.label_column_name
|
|
113
112
|
return nil unless label
|
|
114
113
|
|
|
115
114
|
scope.where(name => LikeSpec.apply(target.all, [label], value))
|
|
@@ -49,7 +49,7 @@ module CrudComponents
|
|
|
49
49
|
# Default ?q= reaches the children's label (the names shown in the list).
|
|
50
50
|
# Skipped when the target's label is a block/columnless.
|
|
51
51
|
def search_spec_entry
|
|
52
|
-
name if target_structure.
|
|
52
|
+
name if target_structure.label_column_name
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# ── filter ─────────────────────────────────────────────────────────────
|
|
@@ -58,7 +58,7 @@ module CrudComponents
|
|
|
58
58
|
# one of its children does. Skipped when the target's label is a
|
|
59
59
|
# block/columnless (no single column to match). Mirrors belongs_to, which
|
|
60
60
|
# filters by its target's label the same way.
|
|
61
|
-
def derived_filterable? = target_structure.
|
|
61
|
+
def derived_filterable? = target_structure.label_column_name.present?
|
|
62
62
|
|
|
63
63
|
def apply_derived_filter(scope, value: nil, **)
|
|
64
64
|
return scope unless value
|
|
@@ -186,7 +186,7 @@ module CrudComponents
|
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
# The label for an associated record in an association column: a per-column
|
|
189
|
-
# `label:` callable (`attribute :
|
|
189
|
+
# `label:` callable (`attribute :publisher, label: ->(p) { p.display_title(short: true) }`)
|
|
190
190
|
# when given, else the target's default {#crud_label}. Used by the
|
|
191
191
|
# association / association_list renderers so a column can re-title the
|
|
192
192
|
# associated record for its context while keeping the nil-safe link.
|
|
@@ -90,11 +90,11 @@ module CrudComponents
|
|
|
90
90
|
# no single column to match, so ask for the columns explicitly.
|
|
91
91
|
def delegate(model, reflection, path)
|
|
92
92
|
target = reflection.klass
|
|
93
|
-
label = Structure.for(target).
|
|
93
|
+
label = Structure.for(target).label_column_name
|
|
94
94
|
if label.nil?
|
|
95
95
|
raise DefinitionError, "cannot search #{model}##{reflection.name} by label: " \
|
|
96
|
-
"#{target}'s label is a custom block
|
|
97
|
-
"e.g. { #{reflection.name}: %i[...] }"
|
|
96
|
+
"#{target}'s label is not a column (custom block or computed method) — " \
|
|
97
|
+
"spell the columns out, e.g. { #{reflection.name}: %i[...] }"
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
[Entry.new(path + [reflection.name], target, label)]
|
|
@@ -172,6 +172,13 @@ module CrudComponents
|
|
|
172
172
|
label_source.is_a?(Symbol) ? label_source : nil
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
+
# The label as a real database column, else nil (block label, or a Symbol
|
|
176
|
+
# naming a computed method). Only a column can be sorted or LIKE-matched.
|
|
177
|
+
def label_column_name
|
|
178
|
+
name = label_field_name
|
|
179
|
+
name if name && model.column_names.include?(name.to_s)
|
|
180
|
+
end
|
|
181
|
+
|
|
175
182
|
# The icon name (no library prefix) badging this model: the declared `icon`,
|
|
176
183
|
# else the name-based guess in `config.model_icons` (keyed by the singular
|
|
177
184
|
# underscored model name), else `config.model_fallback_icon` (nil = none).
|