Class: CrudComponents::Fields::PathField

Inherits:
ComputedField show all
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

Base::NON_EDITABLE_COLUMNS

Instance Attribute Summary

Attributes inherited from Base

#facets, #model, #name, #options

Instance Method Summary collapse

Methods inherited from ComputedField

#default_renderer

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, options = {}, 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

Returns:

  • (Boolean)


186
# File 'lib/crud_components/fields/path_field.rb', line 186

def collection? = reflections.any?(&:collection?)

#eager_loadObject

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_controlObject



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

Returns:

  • (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.

Returns:

  • (Boolean)


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_labelObject



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_modelObject

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_nameObject

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 options[:label] if options[: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

Returns:

  • (Boolean)


152
153
154
# File 'lib/crud_components/fields/path_field.rb', line 152

def nullable?
  delegating? ? target_field.nullable? : super
end

#picker_labelObject



113
# File 'lib/crud_components/fields/path_field.rb', line 113

def picker_label = target_model.human_attribute_name(attribute_name)

#render_blockObject

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 options[:as] if options[:as]
  return nil if render_block

  return target_field.renderer if delegating?

  super
end

#renderer_optionsObject

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 renderer_options
  own = super
  delegating? ? target_field.renderer_options.merge(own) : own
end

#single?Boolean

Returns:

  • (Boolean)


187
# File 'lib/crud_components/fields/path_field.rb', line 187

def single? = !collection?

#sortable?Boolean

── sorting: single-valued paths only ────────────────────────────────────

Returns:

  • (Boolean)


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