Class: CrudComponents::Fields::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/crud_components/fields/base.rb

Overview

One subclass per field flavor (one row of the README's combination table). A field knows how it renders (which partial), how it filters (which control + how params reach SQL), and whether it sorts.

Facets declared in an attribute block override exactly one of those: :render (block), :filter (like-spec / block / false), :sort (column symbol / block / false).

Constant Summary collapse

NON_EDITABLE_COLUMNS =

── forms ────────────────────────────────────────────────────────────── Columns that exist but are never user-editable in a derived form.

%w[id created_at updated_at].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, model, options = {}, facets = {}) ⇒ Base

Returns a new instance of Base.



13
14
15
16
17
18
# File 'lib/crud_components/fields/base.rb', line 13

def initialize(name, model, options = {}, facets = {})
  @name = name.to_sym
  @model = model
  @options = options
  @facets = facets
end

Instance Attribute Details

#facetsObject (readonly)

Returns the value of attribute facets.



11
12
13
# File 'lib/crud_components/fields/base.rb', line 11

def facets
  @facets
end

#modelObject (readonly)

Returns the value of attribute model.



11
12
13
# File 'lib/crud_components/fields/base.rb', line 11

def model
  @model
end

#nameObject (readonly)

Returns the value of attribute name.



11
12
13
# File 'lib/crud_components/fields/base.rb', line 11

def name
  @name
end

#optionsObject (readonly)

Returns the value of attribute options.



11
12
13
# File 'lib/crud_components/fields/base.rb', line 11

def options
  @options
end

Instance Method Details

#apply_derived_filter(scope) ⇒ Object



176
177
178
# File 'lib/crud_components/fields/base.rb', line 176

def apply_derived_filter(scope, **)
  scope
end

#apply_filter(scope, value: nil, geq: nil, leq: nil) ⇒ Object

The raw param values (Strings or nil): value is the bare ?field=, geq and leq the range bounds. How a field reads value is up to it — an exact match for a number/enum, a substring for text.



155
156
157
158
159
160
161
162
163
164
165
# File 'lib/crud_components/fields/base.rb', line 155

def apply_filter(scope, value: nil, geq: nil, leq: nil)
  if typed_filter
    typed_filter.apply(scope, value:, geq:, leq:)
  elsif filter_facet
    return scope unless value

    apply_filter_facet(scope, value)
  else
    apply_derived_filter(scope, value:, geq:, leq:)
  end
end

#apply_filter_facet(scope, value) ⇒ Object



167
168
169
170
171
172
173
174
# File 'lib/crud_components/fields/base.rb', line 167

def apply_filter_facet(scope, value)
  facet = filter_facet
  if facet.is_a?(Proc)
    facet.call(scope.extending(WhereLike), value)
  else
    LikeSpec.apply(scope, facet, value)
  end
end

#apply_sort(scope, dir) ⇒ Object

An explicit ?sort= must win over any order a prior stage set (a search backend's relevance rank, a default scope). The Symbol/derived branches already .reorder; a Proc facet is handed a scope whose prior order is cleared, so a block using the obvious .order(...) overrides the rank too rather than appending to it (see issue #23). A block may still .reorder itself — that composes fine on an already-cleared scope.



203
204
205
206
207
208
209
# File 'lib/crud_components/fields/base.rb', line 203

def apply_sort(scope, dir)
  case (facet = sort_facet)
  when Proc then facet.call(scope.reorder(nil), dir)
  when Symbol then scope.reorder(model.arel_table[facet].public_send(dir))
  else scope.reorder(model.arel_table[name].public_send(dir))
  end
end

#columnObject

The DB column backing this field, if any (nil for associations and computed fields).



55
56
57
# File 'lib/crud_components/fields/base.rb', line 55

def column
  model.columns_hash[name.to_s]
end

#custom_header?Boolean

Whether this column brings its own header markup or header actions — the layout falls back to the plain human_name + sort link when it doesn't.

Returns:

  • (Boolean)


39
# File 'lib/crud_components/fields/base.rb', line 39

def custom_header? = !header.nil? || header_actions.any?

#declared_preloadsObject

The preload: option as an array of includes-specs (a nested hash kept intact, unlike Array()).



271
272
273
274
275
276
277
# File 'lib/crud_components/fields/base.rb', line 271

def declared_preloads
  case (p = options[:preload])
  when nil then []
  when Array then p
  else [p]
  end
end

#default_editable?Boolean

Returns:

  • (Boolean)


226
227
228
# File 'lib/crud_components/fields/base.rb', line 226

def default_editable?
  false
end

#default_rendererObject



87
88
89
# File 'lib/crud_components/fields/base.rb', line 87

def default_renderer
  :string
end

#derived_filter_controlObject



140
141
142
# File 'lib/crud_components/fields/base.rb', line 140

def derived_filter_control
  :text
end

#derived_filterable?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'lib/crud_components/fields/base.rb', line 128

def derived_filterable?
  false
end

#derived_sortable?Boolean

Returns:

  • (Boolean)


193
194
195
# File 'lib/crud_components/fields/base.rb', line 193

def derived_sortable?
  false
end

#eager_loadObject

── loading ────────────────────────────────────────────────────────── Includes-specs (symbols/nested hashes for ActiveRecord#includes) to eager-load when this column is shown. Base contributes the per-attribute preload: — associations a render block / custom renderer reaches on the listed model. Association fields override to also nest the target's identity_preloads under the association name.



265
266
267
# File 'lib/crud_components/fields/base.rb', line 265

def eager_load
  declared_preloads
end

#editable?Boolean

Whether this field appears as an input in a derived form. editable: overrides; a symbol/Proc means "editable, subject to a can? check" (see editable_permitted?).

Returns:

  • (Boolean)


218
219
220
221
222
223
224
# File 'lib/crud_components/fields/base.rb', line 218

def editable?
  case options[:editable]
  when false then false
  when nil then default_editable?
  else true
  end
end

#editable_permitted?(context, record = nil) ⇒ Boolean

Returns:

  • (Boolean)


230
231
232
233
234
235
236
237
238
# File 'lib/crud_components/fields/base.rb', line 230

def editable_permitted?(context, record = nil)
  condition = options[:editable]
  return true unless condition.is_a?(Symbol) || condition.is_a?(Proc)

  # recordless: false — a record-dependent `editable:` can't be granted by
  # the class-level permit list (no record there); deny by default and let
  # the per-record form check decide where a record is present.
  Permission.permitted?(condition, model, context, record, recordless: false)
end

#filter_choices(query = nil) ⇒ Object



144
145
146
# File 'lib/crud_components/fields/base.rb', line 144

def filter_choices(query = nil)
  typed_filter&.filter_choices(query)
end

#filter_controlObject

Which filter control partial to render: :text, :select, :boolean, :number_range or :date_range.



134
135
136
137
138
# File 'lib/crud_components/fields/base.rb', line 134

def filter_control
  return typed_filter.control if typed_filter

  filter_facet ? :text : derived_filter_control
end

#filter_facetObject



113
114
115
116
# File 'lib/crud_components/fields/base.rb', line 113

def filter_facet
  facets[:filter].is_a?(Proc) || facets[:filter].is_a?(Array) ||
    facets[:filter].is_a?(Hash) || facets[:filter].is_a?(Symbol) ? facets[:filter] : nil
end

#filter_includes_null?Boolean

Whether this field's filter offers a "not set" (IS NULL) choice.

Returns:

  • (Boolean)


74
75
76
# File 'lib/crud_components/fields/base.rb', line 74

def filter_includes_null?
  false
end

#filterable?Boolean

── filtering ────────────────────────────────────────────────────────

Returns:

  • (Boolean)


105
106
107
108
109
110
111
# File 'lib/crud_components/fields/base.rb', line 105

def filterable?
  return false if facets[:filter] == false
  return false if CrudComponents::RESERVED_PARAMS.include?(name.to_s)
  return true if typed_filter || filter_facet

  derived_filterable?
end

#form_controlObject

The form-input flavor; nil = no form representation (json, computed).



241
242
243
# File 'lib/crud_components/fields/base.rb', line 241

def form_control
  nil
end

#form_partialObject

The form-input partial to render: crud_components/form_fields/_. Defaults to the field's form_control type; override per field with form_as: (mirrors as: for the read-only/display renderer). The partial receives the simple_form builder f, the field, and form.



249
250
251
# File 'lib/crud_components/fields/base.rb', line 249

def form_partial
  options[:form_as] || form_control
end

#group_labelObject

Column-picker grouping: the heading this column sits under (a path column groups under the association(s) it reaches through), or nil for an own column. picker_label is the label shown within that group.



44
# File 'lib/crud_components/fields/base.rb', line 44

def group_label = nil

#group_modelObject

The model the column-picker groups this column under (Pipedrive-style): its own model for a plain column, the associated model for an association or path column — so publisher, publisher.name and publisher.founded_on all sit under "Publisher".



51
# File 'lib/crud_components/fields/base.rb', line 51

def group_model = model

#headerObject

── column header (issue #4) ─────────────────────────────────────────── A column may own its <th>: a custom header: (a String rendered as-is — mark it html_safe for markup — or a view-context block, e.g. a link), and header_actions: — plain Action objects rendered in the header. A :selection action there acts on the ticked rows (it submits the shared select-form); any other action renders as a link/button. Available on every field flavor: a DynamicColumn passes them through its options, a declared attribute takes them as options too.



34
# File 'lib/crud_components/fields/base.rb', line 34

def header = options[:header]

#header_actionsObject



35
# File 'lib/crud_components/fields/base.rb', line 35

def header_actions = Array(options[:header_actions])

#human_nameObject



20
21
22
23
24
# File 'lib/crud_components/fields/base.rb', line 20

def human_name
  return options[:label] if options[:label].is_a?(String)

  model.human_attribute_name(name)
end

#nullable?Boolean

Whether the backing column permits NULL — gates the "not set" filter choice and the 3-state form control for nullable boolean/enum fields.

Returns:

  • (Boolean)


69
70
71
# File 'lib/crud_components/fields/base.rb', line 69

def nullable?
  !!column&.null
end

#permit_paramObject

What this field contributes to a strong-params permit list — a symbol or a nested hash; collected by Structure#permitted_params.



255
256
257
# File 'lib/crud_components/fields/base.rb', line 255

def permit_param
  name
end

#permitted?(context, record = nil) ⇒ Boolean

── permissions ──────────────────────────────────────────────────────

Returns:

  • (Boolean)


100
101
102
# File 'lib/crud_components/fields/base.rb', line 100

def permitted?(context, record = nil)
  Permission.permitted?(options[:if], model, context, record)
end

#picker_labelObject



45
# File 'lib/crud_components/fields/base.rb', line 45

def picker_label = human_name

#range_filter?Boolean

Returns:

  • (Boolean)


148
149
150
# File 'lib/crud_components/fields/base.rb', line 148

def range_filter?
  filter_control == :number_range || filter_control == :date_range
end

#render_blockObject



91
92
93
# File 'lib/crud_components/fields/base.rb', line 91

def render_block
  facets[:render]
end

#renderer(_record = nil) ⇒ Object

── rendering ────────────────────────────────────────────────────────



83
84
85
# File 'lib/crud_components/fields/base.rb', line 83

def renderer(_record = nil)
  options[:as] || default_renderer
end

#renderer_optionsObject



95
96
97
# File 'lib/crud_components/fields/base.rb', line 95

def renderer_options
  options.except(:as, :if, :form_as, :label, :header, :header_actions, :filter_as, :filter_choices)
end

#search_spec_entryObject

How this field feeds the default ?q= ("search what you see"): its own string/text column, or nil for fields with no free-text column behind them (numbers, dates, enums, attachments, computed). Associations override this to contribute their target's label.



63
64
65
# File 'lib/crud_components/fields/base.rb', line 63

def search_spec_entry
  name if column && %i[string text].include?(column.type)
end

#sort_facetObject



189
190
191
# File 'lib/crud_components/fields/base.rb', line 189

def sort_facet
  facets[:sort].is_a?(Proc) || facets[:sort].is_a?(Symbol) ? facets[:sort] : nil
end

#sortable?Boolean

── sorting ──────────────────────────────────────────────────────────

Returns:

  • (Boolean)


181
182
183
184
185
186
187
# File 'lib/crud_components/fields/base.rb', line 181

def sortable?
  return false if facets[:sort] == false
  return false if CrudComponents::RESERVED_PARAMS.include?(name.to_s)
  return true if sort_facet

  derived_sortable?
end

#typed_filterObject

The internal TypedFilter for a filter: block that declares keyword params (eq:/geq:/leq:/contains:) — its value type comes from filter_as: or as:, so it renders the matching control and receives cast values. A positional ->(scope, value) block has none (plain text filter); nil for everyone else.



122
123
124
125
126
# File 'lib/crud_components/fields/base.rb', line 122

def typed_filter
  return @typed_filter if defined?(@typed_filter)

  @typed_filter = build_typed_filter
end

#value(record) ⇒ Object



78
79
80
# File 'lib/crud_components/fields/base.rb', line 78

def value(record)
  record.public_send(name)
end