Class: CrudComponents::Fields::PathField
- Inherits:
-
ComputedField
- Object
- Base
- ComputedField
- CrudComponents::Fields::PathField
- Defined in:
- lib/crud_components/fields/path_field.rb
Overview
A column that reaches through associations: a dotted name like ‘publisher.name` or `authors.email`. The leading segments are associations on the model; the last is an attribute (or method) on the target.
A single-valued path (belongs_to / has_one) **delegates to the target model’s own field** for that attribute: ‘publisher.founded_on` renders, filters and sorts exactly like Publisher’s ‘founded_on` column does — a date cell, a date-range filter, an ORDER BY the date — and `publisher.price` keeps the target’s ‘unit:`/`digits:` formatting. The path column can still override any of it (`as:`, a `filter`/`sort`/`render` facet, or its own options) —**override > target field > default**. A collection path (has_many / habtm) renders the values as a list and filters by contains-match through the join. The association is eager-loaded automatically.
When the leaf attribute is the target’s label field (‘publisher.name`), the cell renders a link to that record — its model icon then a link to its show page — so a path column doubles as a jump-to-the-object.
Two limits (see #validate!): the chain may be at most ‘config.max_path_depth` associations deep, and it may cross **at most one to-many** association —belongs_to/has_one chain freely, but a second has_many/habtm would fan a list out into a meaningless list-of-lists. `habtm → one` (authors.publisher.name) is fine; `habtm → many` is not.
Constant Summary collapse
- SCALAR_TARGETS =
Target field flavors a single-valued path delegates render/filter/sort to. belongs_to/has_one/attachment/json/computed targets keep the path’s own value-type rendering and contains-match filtering (no delegation).
[StringField, TextField, NumericField, DateField, BooleanField, EnumField].freeze
Constants inherited from Base
Instance Attribute Summary
Attributes inherited from Base
#facets, #model, #name, #options
Instance Method Summary collapse
- #apply_filter(scope, exact: nil, geq: nil, leq: nil) ⇒ Object
- #apply_sort(scope, dir) ⇒ Object
- #collection? ⇒ Boolean
-
#eager_load ⇒ Object
Eager-load the association chain so a whole page costs one query, not one per row (e.g. ‘publisher.founded_on` → includes(:publisher)).
- #filter_choices(query = nil) ⇒ Object
- #filter_control ⇒ Object
- #filter_includes_null? ⇒ Boolean
-
#filterable? ⇒ Boolean
── filtering ───────────────────────────────────────────────────────────── Single-valued scalar paths offer the target field’s own control (a date range, an enum select, …) and apply it through the association; collection / non-scalar paths keep the safe contains-match.
- #group_label ⇒ Object
-
#group_model ⇒ Object
Picker grouping: a path column sits under its target model’s group, next to the association column that anchors it.
-
#human_name ⇒ Object
Header: a breadcrumb “Parent › Attribute” (Pipedrive-style).
-
#human_value(value) ⇒ Object
The target field humanizes its own values (enum labels); a path delegates so a ‘publisher.status` cell badges the same text the Publisher table does.
-
#initialize(name, model, options = {}, facets = {}) ⇒ PathField
constructor
A new instance of PathField.
- #nullable? ⇒ Boolean
- #picker_label ⇒ Object
-
#render_block ⇒ Object
Single paths render through the renderer; collection paths render as a joined list, and a path to the target’s label field renders a link.
- #render_list(view, record) ⇒ Object
-
#render_list_label(view, record) ⇒ Object
show page), runs in the view context via the render block.
-
#renderer(record = nil) ⇒ Object
Single value: the target field’s renderer (date/number/email/…) when it’s a scalar column — so the path renders like the target — unless overridden.
-
#renderer_options ⇒ Object
Target-field options (unit/digits/…) as the base, overridden by any the path column declares itself — ‘override > target field`.
- #single? ⇒ Boolean
-
#sortable? ⇒ Boolean
── sorting: single-valued paths only ────────────────────────────────────.
-
#value(record) ⇒ Object
The list of values reached by the path: an Array for a collection path, the single value (or nil) otherwise.
Methods inherited from ComputedField
Methods inherited from Base
#apply_derived_filter, #apply_filter_facet, #column, #custom_header?, #declared_preloads, #default_editable?, #default_renderer, #derived_filter_control, #derived_filterable?, #derived_sortable?, #editable?, #editable_permitted?, #filter_facet, #form_control, #form_partial, #header, #header_actions, #permit_param, #permitted?, #range_filter?, #sort_facet
Constructor Details
#initialize(name, model, options = {}, facets = {}) ⇒ PathField
Returns a new instance of PathField.
33 34 35 36 37 |
# File 'lib/crud_components/fields/path_field.rb', line 33 def initialize(name, model, = {}, facets = {}) super @segments = name.to_s.split('.').map(&:to_sym) validate! end |
Instance Method Details
#apply_filter(scope, exact: nil, geq: nil, leq: nil) ⇒ Object
162 163 164 165 166 167 168 |
# File 'lib/crud_components/fields/path_field.rb', line 162 def apply_filter(scope, exact: nil, geq: nil, leq: nil) return super if filter_facet # an author-supplied facet wins return delegate_filter(scope, exact: exact, geq: geq, leq: leq) if delegating? return scope unless exact LikeSpec.apply(scope, filter_spec, exact) end |
#apply_sort(scope, dir) ⇒ Object
178 179 180 181 182 183 184 |
# File 'lib/crud_components/fields/path_field.rb', line 178 def apply_sort(scope, dir) return super if sort_facet joins = eager_load.first scope = scope.left_joins(joins) if joins scope.reorder(target_model.arel_table[attribute_name].public_send(dir)) end |
#collection? ⇒ Boolean
186 |
# File 'lib/crud_components/fields/path_field.rb', line 186 def collection? = reflections.any?(&:collection?) |
#eager_load ⇒ Object
Eager-load the association chain so a whole page costs one query, not one per row (e.g. ‘publisher.founded_on` → includes(:publisher)).
121 122 123 124 |
# File 'lib/crud_components/fields/path_field.rb', line 121 def eager_load spec = assoc_segments.reverse.reduce(nil) { |inner, seg| inner ? { seg => inner } : seg } spec ? [spec] : [] end |
#filter_choices(query = nil) ⇒ Object
142 143 144 145 146 |
# File 'lib/crud_components/fields/path_field.rb', line 142 def filter_choices(query = nil) return nil unless delegating? && !filter_facet target_field.filter_choices(query) end |
#filter_control ⇒ Object
137 138 139 140 |
# File 'lib/crud_components/fields/path_field.rb', line 137 def filter_control return :text if filter_facet delegating? ? target_field.filter_control : :text end |
#filter_includes_null? ⇒ Boolean
148 149 150 |
# File 'lib/crud_components/fields/path_field.rb', line 148 def filter_includes_null? delegating? && !filter_facet ? target_field.filter_includes_null? : false end |
#filterable? ⇒ Boolean
── filtering ─────────────────────────────────────────────────────────────Single-valued scalar paths offer the target field’s own control (a date range, an enum select, …) and apply it through the association; collection / non-scalar paths keep the safe contains-match.
130 131 132 133 134 135 |
# File 'lib/crud_components/fields/path_field.rb', line 130 def filterable? return false if facets[:filter] == false return false if CrudComponents::RESERVED_PARAMS.include?(name.to_s) true end |
#group_label ⇒ Object
109 110 111 |
# File 'lib/crud_components/fields/path_field.rb', line 109 def group_label reflections.map { |ref| ref.active_record.human_attribute_name(ref.name) }.join(' › ') end |
#group_model ⇒ Object
Picker grouping: a path column sits under its target model’s group, next to the association column that anchors it.
117 |
# File 'lib/crud_components/fields/path_field.rb', line 117 def group_model = target_model |
#human_name ⇒ Object
Header: a breadcrumb “Parent › Attribute” (Pipedrive-style). The picker groups by ‘group_label` and shows the short `picker_label`, so it isn’t repeated there.
103 104 105 106 107 |
# File 'lib/crud_components/fields/path_field.rb', line 103 def human_name return [:label] if [:label].is_a?(String) "#{group_label} › #{picker_label}" end |
#human_value(value) ⇒ Object
The target field humanizes its own values (enum labels); a path delegates so a ‘publisher.status` cell badges the same text the Publisher table does.
158 159 160 |
# File 'lib/crud_components/fields/path_field.rb', line 158 def human_value(value) delegating? && target_field.respond_to?(:human_value) ? target_field.human_value(value) : value end |
#nullable? ⇒ Boolean
152 153 154 |
# File 'lib/crud_components/fields/path_field.rb', line 152 def nullable? delegating? ? target_field.nullable? : super end |
#picker_label ⇒ Object
113 |
# File 'lib/crud_components/fields/path_field.rb', line 113 def picker_label = target_model.human_attribute_name(attribute_name) |
#render_block ⇒ Object
Single paths render through the renderer; collection paths render as a joined list, and a path to the target’s label field renders a link.
68 69 70 71 72 73 74 |
# File 'lib/crud_components/fields/path_field.rb', line 68 def render_block return facets[:render] if facets[:render] return list_renderer if collection? return label_link_renderer if link_to_target? nil end |
#render_list(view, record) ⇒ Object
77 78 79 80 81 82 83 84 |
# File 'lib/crud_components/fields/path_field.rb', line 77 def render_list(view, record) items = Array(value(record)).map { |v| v.to_s.strip }.reject(&:blank?) return view.tag.span('—', class: CrudComponents.config.css.muted) if items.empty? # ask the target's field how it renders (email → mailto, url → link) semantic = target_field&.renderer view.safe_join(items.map { |item| link_value(view, semantic, item) }, ', ') end |
#render_list_label(view, record) ⇒ Object
show page), runs in the view context via the render block.
88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/crud_components/fields/path_field.rb', line 88 def render_list_label(view, record) target = target_record(record) muted = CrudComponents.config.css.muted return view.tag.span('—', class: muted) if target.nil? label = value(record).to_s icon = view.crud_model_icon(target_model) inner = view.safe_join([icon, view.tag.span(label)].compact, icon ? ' ' : '') path = view.crud_record_path(target) path ? view.link_to(inner, path, data: { turbo_action: 'advance' }) : inner end |
#renderer(record = nil) ⇒ Object
Single value: the target field’s renderer (date/number/email/…) when it’s a scalar column — so the path renders like the target — unless overridden. Collection / non-scalar target: fall back to the inferred value-type renderer (ComputedField).
50 51 52 53 54 55 56 57 |
# File 'lib/crud_components/fields/path_field.rb', line 50 def renderer(record = nil) return [:as] if [:as] return nil if render_block return target_field.renderer if delegating? super end |
#renderer_options ⇒ Object
Target-field options (unit/digits/…) as the base, overridden by any the path column declares itself — ‘override > target field`.
61 62 63 64 |
# File 'lib/crud_components/fields/path_field.rb', line 61 def own = super delegating? ? target_field..merge(own) : own end |
#single? ⇒ Boolean
187 |
# File 'lib/crud_components/fields/path_field.rb', line 187 def single? = !collection? |
#sortable? ⇒ Boolean
── sorting: single-valued paths only ────────────────────────────────────
171 172 173 174 175 176 |
# File 'lib/crud_components/fields/path_field.rb', line 171 def sortable? return false if collection? || facets[:sort] == false return false if CrudComponents::RESERVED_PARAMS.include?(name.to_s) true end |
#value(record) ⇒ Object
The list of values reached by the path: an Array for a collection path, the single value (or nil) otherwise.
41 42 43 44 |
# File 'lib/crud_components/fields/path_field.rb', line 41 def value(record) values = leaf_values(record) collection? ? values : values.first end |