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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f9f34326b3293f67ca0c584810204c8cafaa134c9a198133c5e0250a6022363
4
- data.tar.gz: a0e849ee97e6e9f2ce92bf8e3a5fcde8575d82346d1765bc27084ac6ee3a9c1b
3
+ metadata.gz: 3039812d78a4d8677f871959497a18eb9f2236627a84a0d046b72a520c0ba34c
4
+ data.tar.gz: 70b36787f0e28e2f51af0cfdf3361dc860fcdfb048d90365ddf4ebf128759372
5
5
  SHA512:
6
- metadata.gz: ec31fe79476eb57060a317bf48167cae62496ba135e7bc5bf3a2f8da26b5e665e075678fa78bd092c0e2d3ffd630f4e098f378e7f32ecab2b862baffcefb4bbb
7
- data.tar.gz: f3f9d619c8246a57a1a42dae88211cc0155b0f0ae155efcb93f2a79e5be9ad338370c52f4d204d9dccb4b08767fdde13186af2ce13403ec645599d889911d31d
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 Order's
220
- identity once and every `belongs_to :order` column gets it for free. →
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 :participants, :owner, if: :manage
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
- participants × mails / × resources **matrix** live entirely in `crud_collection` — one
272
- `DynamicColumn` per mail/resource, its controls in its own header — instead of a hand-built
273
- controls strip above the table.
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 :full_title, preload: %i[publisher]`
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` (block label: facet) | no | "+n more" links to nested/filtered index |
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 is a block (no
19
- single column to match), opts in with the `filter` facet (a [search spec](#the-search-spec) or
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 is a custom block
99
- (no single column to match), spell the columns out (`filter authors: :name`).
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 :full_title, preload: %i[customer training]`). They're
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 :customer, :training`.
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 (`{ customer: :company }`) intact (Array() would split it).
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.label_field_name
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. { order: %i[customer training] }.
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
- col = target_structure.label_field_name
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.label_field_name
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.label_field_name
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.label_field_name.present?
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 :order, label: ->(o) { o.full_title(short: true) }`)
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).label_field_name
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, not a columnspell the columns out, " \
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).
@@ -1,3 +1,3 @@
1
1
  module CrudComponents
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.2.1'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crud_components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anatoly Zelenin